mirror of
https://github.com/RGBCube/serenity
synced 2025-07-28 11:07:46 +00:00
DevTools: Move to Userland/DevTools/
This commit is contained in:
parent
13d7c09125
commit
4055b03291
125 changed files with 2 additions and 2 deletions
67
Userland/DevTools/HackStudio/AutoCompleteResponse.h
Normal file
67
Userland/DevTools/HackStudio/AutoCompleteResponse.h
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/String.h>
|
||||
#include <AK/Types.h>
|
||||
#include <LibGUI/AutocompleteProvider.h>
|
||||
#include <LibIPC/Decoder.h>
|
||||
#include <LibIPC/Encoder.h>
|
||||
|
||||
namespace IPC {
|
||||
|
||||
template<>
|
||||
inline bool encode(IPC::Encoder& encoder, const GUI::AutocompleteProvider::Entry& response)
|
||||
{
|
||||
encoder << response.completion;
|
||||
encoder << (u64)response.partial_input_length;
|
||||
encoder << (u32)response.kind;
|
||||
encoder << (u32)response.language;
|
||||
return true;
|
||||
}
|
||||
|
||||
template<>
|
||||
inline bool decode(IPC::Decoder& decoder, GUI::AutocompleteProvider::Entry& response)
|
||||
{
|
||||
u32 kind = 0;
|
||||
u32 language = 0;
|
||||
u64 partial_input_length = 0;
|
||||
bool ok = decoder.decode(response.completion)
|
||||
&& decoder.decode(partial_input_length)
|
||||
&& decoder.decode(kind)
|
||||
&& decoder.decode(language);
|
||||
|
||||
if (ok) {
|
||||
response.kind = static_cast<GUI::AutocompleteProvider::CompletionKind>(kind);
|
||||
response.language = static_cast<GUI::AutocompleteProvider::Language>(language);
|
||||
response.partial_input_length = partial_input_length;
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
}
|
37
Userland/DevTools/HackStudio/CMakeLists.txt
Normal file
37
Userland/DevTools/HackStudio/CMakeLists.txt
Normal file
|
@ -0,0 +1,37 @@
|
|||
add_subdirectory(LanguageServers)
|
||||
add_subdirectory(LanguageClients)
|
||||
|
||||
set(SOURCES
|
||||
CodeDocument.cpp
|
||||
CursorTool.cpp
|
||||
Debugger/BacktraceModel.cpp
|
||||
Debugger/DebugInfoWidget.cpp
|
||||
Debugger/Debugger.cpp
|
||||
Debugger/DisassemblyModel.cpp
|
||||
Debugger/DisassemblyWidget.cpp
|
||||
Debugger/RegistersModel.cpp
|
||||
Debugger/VariablesModel.cpp
|
||||
Editor.cpp
|
||||
EditorWrapper.cpp
|
||||
FindInFilesWidget.cpp
|
||||
FormEditorWidget.cpp
|
||||
FormWidget.cpp
|
||||
Git/DiffViewer.cpp
|
||||
Git/GitFilesModel.cpp
|
||||
Git/GitFilesView.cpp
|
||||
Git/GitRepo.cpp
|
||||
Git/GitWidget.cpp
|
||||
HackStudioWidget.cpp
|
||||
LanguageClient.cpp
|
||||
Locator.cpp
|
||||
Project.cpp
|
||||
ProjectFile.cpp
|
||||
TerminalWrapper.cpp
|
||||
WidgetTool.cpp
|
||||
WidgetTreeModel.cpp
|
||||
main.cpp
|
||||
)
|
||||
|
||||
serenity_app(HackStudio ICON app-hack-studio)
|
||||
target_link_libraries(HackStudio LibWeb LibMarkdown LibGUI LibGfx LibCore LibVT LibDebug LibX86 LibDiff LibShell)
|
||||
add_dependencies(HackStudio CppLanguageServer)
|
68
Userland/DevTools/HackStudio/CodeDocument.cpp
Normal file
68
Userland/DevTools/HackStudio/CodeDocument.cpp
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "CodeDocument.h"
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
NonnullRefPtr<CodeDocument> CodeDocument::create(const String& file_path, Client* client)
|
||||
{
|
||||
return adopt(*new CodeDocument(file_path, client));
|
||||
}
|
||||
|
||||
NonnullRefPtr<CodeDocument> CodeDocument::create(Client* client)
|
||||
{
|
||||
return adopt(*new CodeDocument(client));
|
||||
}
|
||||
|
||||
CodeDocument::CodeDocument(const String& file_path, Client* client)
|
||||
: TextDocument(client)
|
||||
, m_file_path(file_path)
|
||||
{
|
||||
LexicalPath lexical_path(file_path);
|
||||
|
||||
if (lexical_path.has_extension(".cpp") || lexical_path.has_extension(".h"))
|
||||
m_language = Language::Cpp;
|
||||
else if (lexical_path.has_extension(".js"))
|
||||
m_language = Language::JavaScript;
|
||||
else if (lexical_path.has_extension(".gml"))
|
||||
m_language = Language::GML;
|
||||
else if (lexical_path.has_extension(".ini"))
|
||||
m_language = Language::Ini;
|
||||
else if (lexical_path.has_extension(".sh"))
|
||||
m_language = Language::Shell;
|
||||
}
|
||||
|
||||
CodeDocument::CodeDocument(Client* client)
|
||||
: TextDocument(client)
|
||||
{
|
||||
}
|
||||
|
||||
CodeDocument::~CodeDocument()
|
||||
{
|
||||
}
|
||||
|
||||
}
|
61
Userland/DevTools/HackStudio/CodeDocument.h
Normal file
61
Userland/DevTools/HackStudio/CodeDocument.h
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Language.h"
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <LibGUI/TextDocument.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class CodeDocument final : public GUI::TextDocument {
|
||||
public:
|
||||
virtual ~CodeDocument() override;
|
||||
static NonnullRefPtr<CodeDocument> create(const String& file_path, Client* client = nullptr);
|
||||
static NonnullRefPtr<CodeDocument> create(Client* client = nullptr);
|
||||
|
||||
const Vector<size_t>& breakpoint_lines() const { return m_breakpoint_lines; }
|
||||
Vector<size_t>& breakpoint_lines() { return m_breakpoint_lines; }
|
||||
Optional<size_t> execution_position() const { return m_execution_position; }
|
||||
void set_execution_position(size_t line) { m_execution_position = line; }
|
||||
void clear_execution_position() { m_execution_position.clear(); }
|
||||
const String& file_path() const { return m_file_path; }
|
||||
Language language() const { return m_language; }
|
||||
|
||||
virtual bool is_code_document() const override final { return true; }
|
||||
|
||||
private:
|
||||
explicit CodeDocument(const String& file_path, Client* client = nullptr);
|
||||
explicit CodeDocument(Client* client = nullptr);
|
||||
|
||||
String m_file_path;
|
||||
Language m_language { Language::Unknown };
|
||||
Vector<size_t> m_breakpoint_lines;
|
||||
Optional<size_t> m_execution_position;
|
||||
};
|
||||
|
||||
}
|
204
Userland/DevTools/HackStudio/CursorTool.cpp
Normal file
204
Userland/DevTools/HackStudio/CursorTool.cpp
Normal file
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "CursorTool.h"
|
||||
#include "FormEditorWidget.h"
|
||||
#include "FormWidget.h"
|
||||
#include "WidgetTreeModel.h"
|
||||
#include <AK/LogStream.h>
|
||||
#include <LibGfx/Palette.h>
|
||||
|
||||
//#define DEBUG_CURSOR_TOOL
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
void CursorTool::on_mousedown(GUI::MouseEvent& event)
|
||||
{
|
||||
#ifdef DEBUG_CURSOR_TOOL
|
||||
dbgln("CursorTool::on_mousedown");
|
||||
#endif
|
||||
auto& form_widget = m_editor.form_widget();
|
||||
auto result = form_widget.hit_test(event.position(), GUI::Widget::ShouldRespectGreediness::No);
|
||||
|
||||
if (event.button() == GUI::MouseButton::Left) {
|
||||
if (result.widget && result.widget != &form_widget) {
|
||||
if (event.modifiers() & Mod_Ctrl) {
|
||||
m_editor.selection().toggle(*result.widget);
|
||||
} else if (!event.modifiers()) {
|
||||
if (!m_editor.selection().contains(*result.widget)) {
|
||||
#ifdef DEBUG_CURSOR_TOOL
|
||||
dbg() << "Selection didn't contain " << *result.widget << ", making it the only selected one";
|
||||
#endif
|
||||
m_editor.selection().set(*result.widget);
|
||||
}
|
||||
|
||||
m_drag_origin = event.position();
|
||||
m_positions_before_drag.clear();
|
||||
m_editor.selection().for_each([&](auto& widget) {
|
||||
m_positions_before_drag.set(&widget, widget.relative_position());
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
m_editor.selection().clear();
|
||||
m_rubber_banding = true;
|
||||
m_rubber_band_origin = event.position();
|
||||
m_rubber_band_position = event.position();
|
||||
form_widget.update();
|
||||
}
|
||||
// FIXME: Do we need to update any part of the FormEditorWidget outside the FormWidget?
|
||||
form_widget.update();
|
||||
}
|
||||
}
|
||||
|
||||
void CursorTool::on_mouseup(GUI::MouseEvent& event)
|
||||
{
|
||||
#ifdef DEBUG_CURSOR_TOOL
|
||||
dbgln("CursorTool::on_mouseup");
|
||||
#endif
|
||||
if (event.button() == GUI::MouseButton::Left) {
|
||||
auto& form_widget = m_editor.form_widget();
|
||||
auto result = form_widget.hit_test(event.position(), GUI::Widget::ShouldRespectGreediness::No);
|
||||
if (!m_dragging && !(event.modifiers() & Mod_Ctrl)) {
|
||||
if (result.widget && result.widget != &form_widget) {
|
||||
m_editor.selection().set(*result.widget);
|
||||
// FIXME: Do we need to update any part of the FormEditorWidget outside the FormWidget?
|
||||
form_widget.update();
|
||||
}
|
||||
}
|
||||
m_dragging = false;
|
||||
m_rubber_banding = false;
|
||||
form_widget.update();
|
||||
}
|
||||
}
|
||||
|
||||
void CursorTool::on_mousemove(GUI::MouseEvent& event)
|
||||
{
|
||||
#ifdef DEBUG_CURSOR_TOOL
|
||||
dbgln("CursorTool::on_mousemove");
|
||||
#endif
|
||||
auto& form_widget = m_editor.form_widget();
|
||||
|
||||
if (m_rubber_banding) {
|
||||
set_rubber_band_position(event.position());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_dragging && event.buttons() & GUI::MouseButton::Left && event.position() != m_drag_origin) {
|
||||
auto result = form_widget.hit_test(event.position(), GUI::Widget::ShouldRespectGreediness::No);
|
||||
if (result.widget && result.widget != &form_widget) {
|
||||
if (!m_editor.selection().contains(*result.widget)) {
|
||||
m_editor.selection().set(*result.widget);
|
||||
// FIXME: Do we need to update any part of the FormEditorWidget outside the FormWidget?
|
||||
form_widget.update();
|
||||
}
|
||||
}
|
||||
m_dragging = true;
|
||||
}
|
||||
|
||||
if (m_dragging) {
|
||||
auto movement_delta = event.position() - m_drag_origin;
|
||||
m_editor.selection().for_each([&](auto& widget) {
|
||||
auto new_rect = widget.relative_rect();
|
||||
new_rect.set_location(m_positions_before_drag.get(&widget).value_or({}).translated(movement_delta));
|
||||
new_rect.set_x(new_rect.x() - (new_rect.x() % m_editor.form_widget().grid_size()));
|
||||
new_rect.set_y(new_rect.y() - (new_rect.y() % m_editor.form_widget().grid_size()));
|
||||
widget.set_relative_rect(new_rect);
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
m_editor.model().update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void CursorTool::on_keydown(GUI::KeyEvent& event)
|
||||
{
|
||||
#ifdef DEBUG_CURSOR_TOOL
|
||||
dbgln("CursorTool::on_keydown");
|
||||
#endif
|
||||
|
||||
auto move_selected_widgets_by = [this](int x, int y) {
|
||||
m_editor.selection().for_each([&](auto& widget) {
|
||||
widget.move_by(x, y);
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
};
|
||||
|
||||
if (event.modifiers() == 0) {
|
||||
switch (event.key()) {
|
||||
case Key_Down:
|
||||
move_selected_widgets_by(0, m_editor.form_widget().grid_size());
|
||||
break;
|
||||
case Key_Up:
|
||||
move_selected_widgets_by(0, -m_editor.form_widget().grid_size());
|
||||
break;
|
||||
case Key_Left:
|
||||
move_selected_widgets_by(-m_editor.form_widget().grid_size(), 0);
|
||||
break;
|
||||
case Key_Right:
|
||||
move_selected_widgets_by(m_editor.form_widget().grid_size(), 0);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CursorTool::set_rubber_band_position(const Gfx::IntPoint& position)
|
||||
{
|
||||
if (m_rubber_band_position == position)
|
||||
return;
|
||||
m_rubber_band_position = position;
|
||||
|
||||
auto rubber_band_rect = this->rubber_band_rect();
|
||||
|
||||
m_editor.selection().clear();
|
||||
m_editor.form_widget().for_each_child_widget([&](auto& child) {
|
||||
if (child.relative_rect().intersects(rubber_band_rect))
|
||||
m_editor.selection().add(child);
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
|
||||
m_editor.form_widget().update();
|
||||
}
|
||||
|
||||
Gfx::IntRect CursorTool::rubber_band_rect() const
|
||||
{
|
||||
if (!m_rubber_banding)
|
||||
return {};
|
||||
return Gfx::IntRect::from_two_points(m_rubber_band_origin, m_rubber_band_position);
|
||||
}
|
||||
|
||||
void CursorTool::on_second_paint(GUI::Painter& painter, GUI::PaintEvent&)
|
||||
{
|
||||
if (!m_rubber_banding)
|
||||
return;
|
||||
auto rect = rubber_band_rect();
|
||||
painter.fill_rect(rect, m_editor.palette().rubber_band_fill());
|
||||
painter.draw_rect(rect, m_editor.palette().rubber_band_border());
|
||||
}
|
||||
|
||||
}
|
64
Userland/DevTools/HackStudio/CursorTool.h
Normal file
64
Userland/DevTools/HackStudio/CursorTool.h
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Tool.h"
|
||||
#include <AK/HashMap.h>
|
||||
#include <LibGUI/Forward.h>
|
||||
#include <LibGfx/Point.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class CursorTool final : public Tool {
|
||||
public:
|
||||
explicit CursorTool(FormEditorWidget& editor)
|
||||
: Tool(editor)
|
||||
{
|
||||
}
|
||||
virtual ~CursorTool() override { }
|
||||
|
||||
private:
|
||||
virtual const char* class_name() const override { return "CursorTool"; }
|
||||
virtual void on_mousedown(GUI::MouseEvent&) override;
|
||||
virtual void on_mouseup(GUI::MouseEvent&) override;
|
||||
virtual void on_mousemove(GUI::MouseEvent&) override;
|
||||
virtual void on_keydown(GUI::KeyEvent&) override;
|
||||
virtual void on_second_paint(GUI::Painter&, GUI::PaintEvent&) override;
|
||||
|
||||
void set_rubber_band_position(const Gfx::IntPoint&);
|
||||
Gfx::IntRect rubber_band_rect() const;
|
||||
|
||||
Gfx::IntPoint m_drag_origin;
|
||||
HashMap<GUI::Widget*, Gfx::IntPoint> m_positions_before_drag;
|
||||
bool m_dragging { false };
|
||||
|
||||
bool m_rubber_banding { false };
|
||||
Gfx::IntPoint m_rubber_band_origin;
|
||||
Gfx::IntPoint m_rubber_band_position;
|
||||
};
|
||||
|
||||
}
|
78
Userland/DevTools/HackStudio/Debugger/BacktraceModel.cpp
Normal file
78
Userland/DevTools/HackStudio/Debugger/BacktraceModel.cpp
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "BacktraceModel.h"
|
||||
#include "Debugger.h"
|
||||
#include <LibDebug/StackFrameUtils.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
NonnullRefPtr<BacktraceModel> BacktraceModel::create(const Debug::DebugSession& debug_session, const PtraceRegisters& regs)
|
||||
{
|
||||
return adopt(*new BacktraceModel(create_backtrace(debug_session, regs)));
|
||||
}
|
||||
|
||||
GUI::Variant BacktraceModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
|
||||
{
|
||||
if (role == GUI::ModelRole::Display) {
|
||||
auto& frame = m_frames.at(index.row());
|
||||
return frame.function_name;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
GUI::ModelIndex BacktraceModel::index(int row, int column, const GUI::ModelIndex&) const
|
||||
{
|
||||
if (row < 0 || row >= static_cast<int>(m_frames.size()))
|
||||
return {};
|
||||
return create_index(row, column, &m_frames.at(row));
|
||||
}
|
||||
|
||||
Vector<BacktraceModel::FrameInfo> BacktraceModel::create_backtrace(const Debug::DebugSession& debug_session, const PtraceRegisters& regs)
|
||||
{
|
||||
u32 current_ebp = regs.ebp;
|
||||
u32 current_instruction = regs.eip;
|
||||
Vector<BacktraceModel::FrameInfo> frames;
|
||||
do {
|
||||
auto lib = debug_session.library_at(regs.eip);
|
||||
if (!lib)
|
||||
continue;
|
||||
String name = lib->debug_info->name_of_containing_function(current_instruction - lib->base_address);
|
||||
if (name.is_null()) {
|
||||
dbgln("BacktraceModel: couldn't find containing function for address: {:p}", current_instruction);
|
||||
name = "<missing>";
|
||||
}
|
||||
|
||||
frames.append({ name, current_instruction, current_ebp });
|
||||
auto frame_info = Debug::StackFrameUtils::get_info(*Debugger::the().session(), current_ebp);
|
||||
ASSERT(frame_info.has_value());
|
||||
current_instruction = frame_info.value().return_address;
|
||||
current_ebp = frame_info.value().next_ebp;
|
||||
} while (current_ebp && current_instruction);
|
||||
return frames;
|
||||
}
|
||||
|
||||
}
|
78
Userland/DevTools/HackStudio/Debugger/BacktraceModel.h
Normal file
78
Userland/DevTools/HackStudio/Debugger/BacktraceModel.h
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGUI/ListView.h>
|
||||
#include <LibGUI/Model.h>
|
||||
#include <sys/arch/i386/regs.h>
|
||||
|
||||
namespace Debug {
|
||||
|
||||
class DebugSession;
|
||||
|
||||
}
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class BacktraceModel final : public GUI::Model {
|
||||
public:
|
||||
static NonnullRefPtr<BacktraceModel> create(const Debug::DebugSession&, const PtraceRegisters& regs);
|
||||
|
||||
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_frames.size(); }
|
||||
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return 1; }
|
||||
|
||||
virtual String column_name(int) const override
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
|
||||
|
||||
virtual void update() override { }
|
||||
virtual GUI::ModelIndex index(int row, int column, const GUI::ModelIndex&) const override;
|
||||
|
||||
struct FrameInfo {
|
||||
String function_name;
|
||||
u32 instruction_address;
|
||||
u32 frame_base;
|
||||
};
|
||||
|
||||
const Vector<FrameInfo>& frames() const { return m_frames; }
|
||||
|
||||
private:
|
||||
explicit BacktraceModel(Vector<FrameInfo>&& frames)
|
||||
: m_frames(move(frames))
|
||||
{
|
||||
}
|
||||
|
||||
static Vector<FrameInfo> create_backtrace(const Debug::DebugSession&, const PtraceRegisters&);
|
||||
|
||||
Vector<FrameInfo> m_frames;
|
||||
};
|
||||
|
||||
}
|
42
Userland/DevTools/HackStudio/Debugger/BreakpointCallback.h
Normal file
42
Userland/DevTools/HackStudio/Debugger/BreakpointCallback.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/Types.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
enum class BreakpointChange {
|
||||
Added,
|
||||
Removed,
|
||||
};
|
||||
|
||||
typedef Function<void(const String& file, size_t line, BreakpointChange)> BreakpointChangeCallback;
|
||||
|
||||
}
|
188
Userland/DevTools/HackStudio/Debugger/DebugInfoWidget.cpp
Normal file
188
Userland/DevTools/HackStudio/Debugger/DebugInfoWidget.cpp
Normal file
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "DebugInfoWidget.h"
|
||||
#include "BacktraceModel.h"
|
||||
#include "Debugger.h"
|
||||
#include "RegistersModel.h"
|
||||
#include "VariablesModel.h"
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibGUI/Action.h>
|
||||
#include <LibGUI/BoxLayout.h>
|
||||
#include <LibGUI/InputBox.h>
|
||||
#include <LibGUI/Layout.h>
|
||||
#include <LibGUI/ListView.h>
|
||||
#include <LibGUI/Menu.h>
|
||||
#include <LibGUI/Model.h>
|
||||
#include <LibGUI/Splitter.h>
|
||||
#include <LibGUI/TabWidget.h>
|
||||
#include <LibGUI/TreeView.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
void DebugInfoWidget::init_toolbar()
|
||||
{
|
||||
m_continue_action = GUI::Action::create("Continue", Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-continue.png"), [](auto&) {
|
||||
Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::Continue);
|
||||
});
|
||||
|
||||
m_singlestep_action = GUI::Action::create("Step Over", { Mod_None, Key_F10 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-step-over.png"), [](auto&) {
|
||||
Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::SourceStepOver);
|
||||
});
|
||||
|
||||
m_step_in_action = GUI::Action::create("Step In", { Mod_None, Key_F11 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-step-in.png"), [](auto&) {
|
||||
Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::SourceSingleStep);
|
||||
});
|
||||
|
||||
m_step_out_action = GUI::Action::create("Step Out", { Mod_Shift, Key_F11 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-step-out.png"), [](auto&) {
|
||||
Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::SourceStepOut);
|
||||
});
|
||||
|
||||
m_toolbar->add_action(*m_continue_action);
|
||||
m_toolbar->add_action(*m_singlestep_action);
|
||||
m_toolbar->add_action(*m_step_in_action);
|
||||
m_toolbar->add_action(*m_step_out_action);
|
||||
|
||||
set_debug_actions_enabled(false);
|
||||
}
|
||||
|
||||
DebugInfoWidget::DebugInfoWidget()
|
||||
{
|
||||
set_layout<GUI::VerticalBoxLayout>();
|
||||
auto& toolbar_container = add<GUI::ToolBarContainer>();
|
||||
m_toolbar = toolbar_container.add<GUI::ToolBar>();
|
||||
init_toolbar();
|
||||
auto& bottom_box = add<GUI::Widget>();
|
||||
bottom_box.set_layout<GUI::HorizontalBoxLayout>();
|
||||
|
||||
auto& splitter = bottom_box.add<GUI::HorizontalSplitter>();
|
||||
m_backtrace_view = splitter.add<GUI::ListView>();
|
||||
auto& variables_tab_widget = splitter.add<GUI::TabWidget>();
|
||||
variables_tab_widget.set_tab_position(GUI::TabWidget::TabPosition::Bottom);
|
||||
variables_tab_widget.add_widget("Variables", build_variables_tab());
|
||||
variables_tab_widget.add_widget("Registers", build_registers_tab());
|
||||
|
||||
m_backtrace_view->on_selection = [this](auto& index) {
|
||||
auto& model = static_cast<BacktraceModel&>(*m_backtrace_view->model());
|
||||
|
||||
// Note: The reconstruction of the register set here is obviously incomplete.
|
||||
// We currently only reconstruct eip & ebp. Ideally would also reconstruct the other registers somehow.
|
||||
// (Other registers may be needed to get the values of variables who are not stored on the stack)
|
||||
PtraceRegisters frame_regs {};
|
||||
frame_regs.eip = model.frames()[index.row()].instruction_address;
|
||||
frame_regs.ebp = model.frames()[index.row()].frame_base;
|
||||
|
||||
m_variables_view->set_model(VariablesModel::create(frame_regs));
|
||||
};
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Widget> DebugInfoWidget::build_variables_tab()
|
||||
{
|
||||
auto variables_widget = GUI::Widget::construct();
|
||||
variables_widget->set_layout<GUI::HorizontalBoxLayout>();
|
||||
|
||||
m_variables_view = variables_widget->add<GUI::TreeView>();
|
||||
|
||||
auto is_valid_index = [](auto& index) {
|
||||
if (!index.is_valid())
|
||||
return false;
|
||||
auto* variable = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data());
|
||||
if (variable->location_type != Debug::DebugInfo::VariableInfo::LocationType::Address)
|
||||
return false;
|
||||
return variable->is_enum_type() || variable->type_name.is_one_of("int", "bool");
|
||||
};
|
||||
|
||||
m_variables_view->on_context_menu_request = [this, is_valid_index](auto& index, auto& event) {
|
||||
if (!is_valid_index(index))
|
||||
return;
|
||||
m_variable_context_menu->popup(event.screen_position());
|
||||
};
|
||||
|
||||
m_variables_view->on_activation = [this, is_valid_index](auto& index) {
|
||||
if (!is_valid_index(index))
|
||||
return;
|
||||
|
||||
String value;
|
||||
if (GUI::InputBox::show(value, window(), "Enter new value:", "Set variable value") == GUI::InputBox::ExecOK) {
|
||||
auto& model = static_cast<VariablesModel&>(*m_variables_view->model());
|
||||
model.set_variable_value(index, value, window());
|
||||
}
|
||||
};
|
||||
|
||||
auto edit_variable_action = GUI::Action::create("Change value", [this](auto&) {
|
||||
m_variables_view->on_activation(m_variables_view->selection().first());
|
||||
});
|
||||
|
||||
m_variable_context_menu = GUI::Menu::construct();
|
||||
m_variable_context_menu->add_action(edit_variable_action);
|
||||
|
||||
return variables_widget;
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Widget> DebugInfoWidget::build_registers_tab()
|
||||
{
|
||||
auto registers_widget = GUI::Widget::construct();
|
||||
registers_widget->set_layout<GUI::HorizontalBoxLayout>();
|
||||
|
||||
m_registers_view = registers_widget->add<GUI::TableView>();
|
||||
|
||||
return registers_widget;
|
||||
}
|
||||
|
||||
void DebugInfoWidget::update_state(const Debug::DebugSession& debug_session, const PtraceRegisters& regs)
|
||||
{
|
||||
m_variables_view->set_model(VariablesModel::create(regs));
|
||||
m_backtrace_view->set_model(BacktraceModel::create(debug_session, regs));
|
||||
if (m_registers_view->model()) {
|
||||
auto& previous_registers = static_cast<RegistersModel*>(m_registers_view->model())->raw_registers();
|
||||
m_registers_view->set_model(RegistersModel::create(regs, previous_registers));
|
||||
} else {
|
||||
m_registers_view->set_model(RegistersModel::create(regs));
|
||||
}
|
||||
auto selected_index = m_backtrace_view->model()->index(0);
|
||||
if (!selected_index.is_valid()) {
|
||||
dbgln("Warning: DebugInfoWidget: backtrace selected index is invalid");
|
||||
return;
|
||||
}
|
||||
m_backtrace_view->selection().set(selected_index);
|
||||
}
|
||||
|
||||
void DebugInfoWidget::program_stopped()
|
||||
{
|
||||
m_variables_view->set_model({});
|
||||
m_backtrace_view->set_model({});
|
||||
m_registers_view->set_model({});
|
||||
}
|
||||
|
||||
void DebugInfoWidget::set_debug_actions_enabled(bool enabled)
|
||||
{
|
||||
m_continue_action->set_enabled(enabled);
|
||||
m_singlestep_action->set_enabled(enabled);
|
||||
m_step_in_action->set_enabled(enabled);
|
||||
m_step_out_action->set_enabled(enabled);
|
||||
}
|
||||
|
||||
}
|
71
Userland/DevTools/HackStudio/Debugger/DebugInfoWidget.h
Normal file
71
Userland/DevTools/HackStudio/Debugger/DebugInfoWidget.h
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Debugger.h"
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <LibGUI/Action.h>
|
||||
#include <LibGUI/ListView.h>
|
||||
#include <LibGUI/Menu.h>
|
||||
#include <LibGUI/Model.h>
|
||||
#include <LibGUI/TableView.h>
|
||||
#include <LibGUI/ToolBar.h>
|
||||
#include <LibGUI/ToolBarContainer.h>
|
||||
#include <LibGUI/TreeView.h>
|
||||
#include <LibGUI/Widget.h>
|
||||
#include <sys/arch/i386/regs.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class DebugInfoWidget final : public GUI::Widget {
|
||||
C_OBJECT(DebugInfoWidget)
|
||||
public:
|
||||
virtual ~DebugInfoWidget() override { }
|
||||
|
||||
void update_state(const Debug::DebugSession&, const PtraceRegisters&);
|
||||
void program_stopped();
|
||||
void set_debug_actions_enabled(bool enabled);
|
||||
|
||||
private:
|
||||
explicit DebugInfoWidget();
|
||||
void init_toolbar();
|
||||
|
||||
NonnullRefPtr<GUI::Widget> build_variables_tab();
|
||||
NonnullRefPtr<GUI::Widget> build_registers_tab();
|
||||
|
||||
RefPtr<GUI::TreeView> m_variables_view;
|
||||
RefPtr<GUI::TableView> m_registers_view;
|
||||
RefPtr<GUI::ListView> m_backtrace_view;
|
||||
RefPtr<GUI::Menu> m_variable_context_menu;
|
||||
RefPtr<GUI::ToolBar> m_toolbar;
|
||||
RefPtr<GUI::Action> m_continue_action;
|
||||
RefPtr<GUI::Action> m_singlestep_action;
|
||||
RefPtr<GUI::Action> m_step_in_action;
|
||||
RefPtr<GUI::Action> m_step_out_action;
|
||||
};
|
||||
|
||||
}
|
295
Userland/DevTools/HackStudio/Debugger/Debugger.cpp
Normal file
295
Userland/DevTools/HackStudio/Debugger/Debugger.cpp
Normal file
|
@ -0,0 +1,295 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "Debugger.h"
|
||||
#include <LibDebug/StackFrameUtils.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
static Debugger* s_the;
|
||||
|
||||
Debugger& Debugger::the()
|
||||
{
|
||||
ASSERT(s_the);
|
||||
return *s_the;
|
||||
}
|
||||
|
||||
void Debugger::initialize(
|
||||
String source_root,
|
||||
Function<HasControlPassedToUser(const PtraceRegisters&)> on_stop_callback,
|
||||
Function<void()> on_continue_callback,
|
||||
Function<void()> on_exit_callback)
|
||||
{
|
||||
s_the = new Debugger(source_root, move(on_stop_callback), move(on_continue_callback), move(on_exit_callback));
|
||||
}
|
||||
|
||||
bool Debugger::is_initialized()
|
||||
{
|
||||
return s_the;
|
||||
}
|
||||
|
||||
Debugger::Debugger(
|
||||
String source_root,
|
||||
Function<HasControlPassedToUser(const PtraceRegisters&)> on_stop_callback,
|
||||
Function<void()> on_continue_callback,
|
||||
Function<void()> on_exit_callback)
|
||||
: m_source_root(source_root)
|
||||
, m_on_stopped_callback(move(on_stop_callback))
|
||||
, m_on_continue_callback(move(on_continue_callback))
|
||||
, m_on_exit_callback(move(on_exit_callback))
|
||||
{
|
||||
pthread_mutex_init(&m_ui_action_mutex, nullptr);
|
||||
pthread_cond_init(&m_ui_action_cond, nullptr);
|
||||
}
|
||||
|
||||
void Debugger::on_breakpoint_change(const String& file, size_t line, BreakpointChange change_type)
|
||||
{
|
||||
auto position = create_source_position(file, line);
|
||||
|
||||
if (change_type == BreakpointChange::Added) {
|
||||
Debugger::the().m_breakpoints.append(position);
|
||||
} else {
|
||||
Debugger::the().m_breakpoints.remove_all_matching([&](Debug::DebugInfo::SourcePosition val) { return val == position; });
|
||||
}
|
||||
|
||||
auto session = Debugger::the().session();
|
||||
if (!session)
|
||||
return;
|
||||
|
||||
auto address = session->get_address_from_source_position(position.file_path, position.line_number);
|
||||
if (!address.has_value()) {
|
||||
dbgln("Warning: couldn't get instruction address from source");
|
||||
// TODO: Currently, the GUI will indicate that a breakpoint was inserted/removed at this line,
|
||||
// regardless of whether we actually succeeded to insert it. (For example a breakpoint on a comment, or an include statement).
|
||||
// We should indicate failure via a return value from this function, and not update the breakpoint GUI if we fail.
|
||||
return;
|
||||
}
|
||||
|
||||
if (change_type == BreakpointChange::Added) {
|
||||
bool success = session->insert_breakpoint(reinterpret_cast<void*>(address.value().address));
|
||||
ASSERT(success);
|
||||
} else {
|
||||
bool success = session->remove_breakpoint(reinterpret_cast<void*>(address.value().address));
|
||||
ASSERT(success);
|
||||
}
|
||||
}
|
||||
|
||||
Debug::DebugInfo::SourcePosition Debugger::create_source_position(const String& file, size_t line)
|
||||
{
|
||||
if (!file.starts_with('/') && !file.starts_with("./"))
|
||||
return { String::formatted("./{}", file), line + 1 };
|
||||
return { file, line + 1 };
|
||||
}
|
||||
|
||||
int Debugger::start_static()
|
||||
{
|
||||
Debugger::the().start();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Debugger::start()
|
||||
{
|
||||
m_debug_session = Debug::DebugSession::exec_and_attach(m_executable_path, m_source_root);
|
||||
ASSERT(!!m_debug_session);
|
||||
|
||||
for (const auto& breakpoint : m_breakpoints) {
|
||||
dbgln("inserting breakpoint at: {}:{}", breakpoint.file_path, breakpoint.line_number);
|
||||
auto address = m_debug_session->get_address_from_source_position(breakpoint.file_path, breakpoint.line_number);
|
||||
if (address.has_value()) {
|
||||
bool success = m_debug_session->insert_breakpoint(reinterpret_cast<void*>(address.value().address));
|
||||
ASSERT(success);
|
||||
} else {
|
||||
dbgln("couldn't insert breakpoint");
|
||||
}
|
||||
}
|
||||
|
||||
debugger_loop();
|
||||
}
|
||||
|
||||
int Debugger::debugger_loop()
|
||||
{
|
||||
ASSERT(m_debug_session);
|
||||
|
||||
m_debug_session->run(Debug::DebugSession::DesiredInitialDebugeeState::Running, [this](Debug::DebugSession::DebugBreakReason reason, Optional<PtraceRegisters> optional_regs) {
|
||||
if (reason == Debug::DebugSession::DebugBreakReason::Exited) {
|
||||
dbgln("Program exited");
|
||||
m_on_exit_callback();
|
||||
return Debug::DebugSession::DebugDecision::Detach;
|
||||
}
|
||||
remove_temporary_breakpoints();
|
||||
ASSERT(optional_regs.has_value());
|
||||
const PtraceRegisters& regs = optional_regs.value();
|
||||
|
||||
auto source_position = m_debug_session->get_source_position(regs.eip);
|
||||
if (!source_position.has_value())
|
||||
return Debug::DebugSession::DebugDecision::SingleStep;
|
||||
|
||||
// We currently do no support stepping through assembly source
|
||||
if (source_position.value().file_path.ends_with(".S"))
|
||||
return Debug::DebugSession::DebugDecision::SingleStep;
|
||||
|
||||
ASSERT(source_position.has_value());
|
||||
if (m_state.get() == Debugger::DebuggingState::SingleStepping) {
|
||||
if (m_state.should_stop_single_stepping(source_position.value())) {
|
||||
m_state.set_normal();
|
||||
} else {
|
||||
return Debug::DebugSession::DebugDecision::SingleStep;
|
||||
}
|
||||
}
|
||||
|
||||
auto control_passed_to_user = m_on_stopped_callback(regs);
|
||||
|
||||
if (control_passed_to_user == HasControlPassedToUser::Yes) {
|
||||
pthread_mutex_lock(&m_ui_action_mutex);
|
||||
pthread_cond_wait(&m_ui_action_cond, &m_ui_action_mutex);
|
||||
pthread_mutex_unlock(&m_ui_action_mutex);
|
||||
|
||||
if (m_requested_debugger_action != DebuggerAction::Exit)
|
||||
m_on_continue_callback();
|
||||
|
||||
} else {
|
||||
m_requested_debugger_action = DebuggerAction::Continue;
|
||||
}
|
||||
|
||||
switch (m_requested_debugger_action) {
|
||||
case DebuggerAction::Continue:
|
||||
m_state.set_normal();
|
||||
return Debug::DebugSession::DebugDecision::Continue;
|
||||
case DebuggerAction::SourceSingleStep:
|
||||
m_state.set_single_stepping(source_position.value());
|
||||
return Debug::DebugSession::DebugDecision::SingleStep;
|
||||
case DebuggerAction::SourceStepOut:
|
||||
m_state.set_stepping_out();
|
||||
do_step_out(regs);
|
||||
return Debug::DebugSession::DebugDecision::Continue;
|
||||
case DebuggerAction::SourceStepOver:
|
||||
m_state.set_stepping_over();
|
||||
do_step_over(regs);
|
||||
return Debug::DebugSession::DebugDecision::Continue;
|
||||
case DebuggerAction::Exit:
|
||||
// NOTE: Is detaching from the debuggee the best thing to do here?
|
||||
// We could display a dialog in the UI, remind the user that there is
|
||||
// a live debugged process, and ask whether they want to terminate/detach.
|
||||
dbgln("Debugger exiting");
|
||||
return Debug::DebugSession::DebugDecision::Detach;
|
||||
}
|
||||
ASSERT_NOT_REACHED();
|
||||
});
|
||||
m_debug_session.clear();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Debugger::DebuggingState::set_normal()
|
||||
{
|
||||
m_state = State::Normal;
|
||||
m_original_source_position.clear();
|
||||
}
|
||||
|
||||
void Debugger::DebuggingState::set_single_stepping(Debug::DebugInfo::SourcePosition original_source_position)
|
||||
{
|
||||
m_state = State::SingleStepping;
|
||||
m_original_source_position = original_source_position;
|
||||
}
|
||||
|
||||
bool Debugger::DebuggingState::should_stop_single_stepping(const Debug::DebugInfo::SourcePosition& current_source_position) const
|
||||
{
|
||||
ASSERT(m_state == State::SingleStepping);
|
||||
return m_original_source_position.value() != current_source_position;
|
||||
}
|
||||
|
||||
void Debugger::remove_temporary_breakpoints()
|
||||
{
|
||||
for (auto breakpoint_address : m_state.temporary_breakpoints()) {
|
||||
ASSERT(m_debug_session->breakpoint_exists((void*)breakpoint_address));
|
||||
bool rc = m_debug_session->remove_breakpoint((void*)breakpoint_address);
|
||||
ASSERT(rc);
|
||||
}
|
||||
m_state.clear_temporary_breakpoints();
|
||||
}
|
||||
|
||||
void Debugger::DebuggingState::clear_temporary_breakpoints()
|
||||
{
|
||||
m_addresses_of_temporary_breakpoints.clear();
|
||||
}
|
||||
void Debugger::DebuggingState::add_temporary_breakpoint(u32 address)
|
||||
{
|
||||
m_addresses_of_temporary_breakpoints.append(address);
|
||||
}
|
||||
|
||||
void Debugger::do_step_out(const PtraceRegisters& regs)
|
||||
{
|
||||
// To step out, we simply insert a temporary breakpoint at the
|
||||
// instruction the current function returns to, and continue
|
||||
// execution until we hit that instruction (or some other breakpoint).
|
||||
insert_temporary_breakpoint_at_return_address(regs);
|
||||
}
|
||||
|
||||
void Debugger::do_step_over(const PtraceRegisters& regs)
|
||||
{
|
||||
// To step over, we insert a temporary breakpoint at each line in the current function,
|
||||
// as well as at the current function's return point, and continue execution.
|
||||
auto lib = m_debug_session->library_at(regs.eip);
|
||||
if (!lib)
|
||||
return;
|
||||
auto current_function = lib->debug_info->get_containing_function(regs.eip - lib->base_address);
|
||||
if (!current_function.has_value()) {
|
||||
dbgln("cannot perform step_over, failed to find containing function of: {:p}", regs.eip);
|
||||
return;
|
||||
}
|
||||
ASSERT(current_function.has_value());
|
||||
auto lines_in_current_function = lib->debug_info->source_lines_in_scope(current_function.value());
|
||||
for (const auto& line : lines_in_current_function) {
|
||||
insert_temporary_breakpoint(line.address_of_first_statement.value() + lib->base_address);
|
||||
}
|
||||
insert_temporary_breakpoint_at_return_address(regs);
|
||||
}
|
||||
|
||||
void Debugger::insert_temporary_breakpoint_at_return_address(const PtraceRegisters& regs)
|
||||
{
|
||||
auto frame_info = Debug::StackFrameUtils::get_info(*m_debug_session, regs.ebp);
|
||||
ASSERT(frame_info.has_value());
|
||||
u32 return_address = frame_info.value().return_address;
|
||||
insert_temporary_breakpoint(return_address);
|
||||
}
|
||||
|
||||
void Debugger::insert_temporary_breakpoint(FlatPtr address)
|
||||
{
|
||||
if (m_debug_session->breakpoint_exists((void*)address))
|
||||
return;
|
||||
bool success = m_debug_session->insert_breakpoint(reinterpret_cast<void*>(address));
|
||||
ASSERT(success);
|
||||
m_state.add_temporary_breakpoint(address);
|
||||
}
|
||||
|
||||
void Debugger::set_requested_debugger_action(DebuggerAction action)
|
||||
{
|
||||
pthread_mutex_lock(continue_mutex());
|
||||
m_requested_debugger_action = action;
|
||||
pthread_cond_signal(continue_cond());
|
||||
pthread_mutex_unlock(continue_mutex());
|
||||
}
|
||||
|
||||
}
|
139
Userland/DevTools/HackStudio/Debugger/Debugger.h
Normal file
139
Userland/DevTools/HackStudio/Debugger/Debugger.h
Normal file
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BreakpointCallback.h"
|
||||
#include <AK/Function.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibDebug/DebugSession.h>
|
||||
#include <LibThread/Lock.h>
|
||||
#include <LibThread/Thread.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class Debugger {
|
||||
public:
|
||||
static Debugger& the();
|
||||
|
||||
enum class HasControlPassedToUser {
|
||||
No,
|
||||
Yes,
|
||||
};
|
||||
|
||||
static void initialize(
|
||||
String source_root,
|
||||
Function<HasControlPassedToUser(const PtraceRegisters&)> on_stop_callback,
|
||||
Function<void()> on_continue_callback,
|
||||
Function<void()> on_exit_callback);
|
||||
|
||||
static bool is_initialized();
|
||||
|
||||
static void on_breakpoint_change(const String& file, size_t line, BreakpointChange change_type);
|
||||
|
||||
void set_executable_path(const String& path) { m_executable_path = path; }
|
||||
|
||||
Debug::DebugSession* session() { return m_debug_session.ptr(); }
|
||||
|
||||
// Thread entry point
|
||||
static int start_static();
|
||||
|
||||
pthread_mutex_t* continue_mutex() { return &m_ui_action_mutex; }
|
||||
pthread_cond_t* continue_cond() { return &m_ui_action_cond; }
|
||||
|
||||
enum class DebuggerAction {
|
||||
Continue,
|
||||
SourceSingleStep,
|
||||
SourceStepOut,
|
||||
SourceStepOver,
|
||||
Exit,
|
||||
};
|
||||
|
||||
void set_requested_debugger_action(DebuggerAction);
|
||||
void reset_breakpoints() { m_breakpoints.clear(); }
|
||||
|
||||
private:
|
||||
class DebuggingState {
|
||||
public:
|
||||
enum State {
|
||||
Normal, // Continue normally until we hit a breakpoint / program terminates
|
||||
SingleStepping,
|
||||
SteppingOut,
|
||||
SteppingOver,
|
||||
};
|
||||
State get() const { return m_state; }
|
||||
|
||||
void set_normal();
|
||||
void set_single_stepping(Debug::DebugInfo::SourcePosition original_source_position);
|
||||
void set_stepping_out() { m_state = State::SteppingOut; }
|
||||
void set_stepping_over() { m_state = State::SteppingOver; }
|
||||
|
||||
bool should_stop_single_stepping(const Debug::DebugInfo::SourcePosition& current_source_position) const;
|
||||
void clear_temporary_breakpoints();
|
||||
void add_temporary_breakpoint(u32 address);
|
||||
const Vector<u32>& temporary_breakpoints() const { return m_addresses_of_temporary_breakpoints; }
|
||||
|
||||
private:
|
||||
State m_state { Normal };
|
||||
Optional<Debug::DebugInfo::SourcePosition> m_original_source_position; // The source position at which we started the current single step
|
||||
Vector<u32> m_addresses_of_temporary_breakpoints;
|
||||
};
|
||||
|
||||
explicit Debugger(
|
||||
String source_root,
|
||||
Function<HasControlPassedToUser(const PtraceRegisters&)> on_stop_callback,
|
||||
Function<void()> on_continue_callback,
|
||||
Function<void()> on_exit_callback);
|
||||
|
||||
static Debug::DebugInfo::SourcePosition create_source_position(const String& file, size_t line);
|
||||
|
||||
void start();
|
||||
int debugger_loop();
|
||||
|
||||
void remove_temporary_breakpoints();
|
||||
void do_step_out(const PtraceRegisters&);
|
||||
void do_step_over(const PtraceRegisters&);
|
||||
void insert_temporary_breakpoint(FlatPtr address);
|
||||
void insert_temporary_breakpoint_at_return_address(const PtraceRegisters&);
|
||||
|
||||
OwnPtr<Debug::DebugSession> m_debug_session;
|
||||
String m_source_root;
|
||||
DebuggingState m_state;
|
||||
|
||||
pthread_mutex_t m_ui_action_mutex {};
|
||||
pthread_cond_t m_ui_action_cond {};
|
||||
DebuggerAction m_requested_debugger_action { DebuggerAction::Continue };
|
||||
|
||||
Vector<Debug::DebugInfo::SourcePosition> m_breakpoints;
|
||||
|
||||
String m_executable_path;
|
||||
|
||||
Function<HasControlPassedToUser(const PtraceRegisters&)> m_on_stopped_callback;
|
||||
Function<void()> m_on_continue_callback;
|
||||
Function<void()> m_on_exit_callback;
|
||||
};
|
||||
|
||||
}
|
137
Userland/DevTools/HackStudio/Debugger/DisassemblyModel.cpp
Normal file
137
Userland/DevTools/HackStudio/Debugger/DisassemblyModel.cpp
Normal file
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "DisassemblyModel.h"
|
||||
#include <AK/MappedFile.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibDebug/DebugSession.h>
|
||||
#include <LibELF/Image.h>
|
||||
#include <LibX86/Disassembler.h>
|
||||
#include <LibX86/ELFSymbolProvider.h>
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
DisassemblyModel::DisassemblyModel(const Debug::DebugSession& debug_session, const PtraceRegisters& regs)
|
||||
{
|
||||
auto lib = debug_session.library_at(regs.eip);
|
||||
if (!lib)
|
||||
return;
|
||||
auto containing_function = lib->debug_info->get_containing_function(regs.eip - lib->base_address);
|
||||
if (!containing_function.has_value()) {
|
||||
dbgln("Cannot disassemble as the containing function was not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
OwnPtr<ELF::Image> kernel_elf;
|
||||
const ELF::Image* elf = nullptr;
|
||||
|
||||
if (containing_function.value().address_low >= 0xc0000000) {
|
||||
auto file_or_error = MappedFile::map("/boot/Kernel");
|
||||
if (file_or_error.is_error())
|
||||
return;
|
||||
kernel_elf = make<ELF::Image>(file_or_error.value()->bytes());
|
||||
elf = kernel_elf.ptr();
|
||||
} else {
|
||||
elf = &lib->debug_info->elf();
|
||||
}
|
||||
|
||||
auto symbol = elf->find_symbol(containing_function.value().address_low);
|
||||
if (!symbol.has_value())
|
||||
return;
|
||||
ASSERT(symbol.has_value());
|
||||
|
||||
auto view = symbol.value().raw_data();
|
||||
|
||||
X86::ELFSymbolProvider symbol_provider(*elf);
|
||||
X86::SimpleInstructionStream stream((const u8*)view.characters_without_null_termination(), view.length());
|
||||
X86::Disassembler disassembler(stream);
|
||||
|
||||
size_t offset_into_symbol = 0;
|
||||
for (;;) {
|
||||
auto insn = disassembler.next();
|
||||
if (!insn.has_value())
|
||||
break;
|
||||
FlatPtr address_in_profiled_program = symbol.value().value() + offset_into_symbol;
|
||||
auto disassembly = insn.value().to_string(address_in_profiled_program, &symbol_provider);
|
||||
StringView instruction_bytes = view.substring_view(offset_into_symbol, insn.value().length());
|
||||
m_instructions.append({ insn.value(), disassembly, instruction_bytes, address_in_profiled_program });
|
||||
|
||||
offset_into_symbol += insn.value().length();
|
||||
}
|
||||
}
|
||||
|
||||
DisassemblyModel::~DisassemblyModel()
|
||||
{
|
||||
}
|
||||
|
||||
int DisassemblyModel::row_count(const GUI::ModelIndex&) const
|
||||
{
|
||||
return m_instructions.size();
|
||||
}
|
||||
|
||||
String DisassemblyModel::column_name(int column) const
|
||||
{
|
||||
switch (column) {
|
||||
case Column::Address:
|
||||
return "Address";
|
||||
case Column::InstructionBytes:
|
||||
return "Insn Bytes";
|
||||
case Column::Disassembly:
|
||||
return "Disassembly";
|
||||
default:
|
||||
ASSERT_NOT_REACHED();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
GUI::Variant DisassemblyModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
|
||||
{
|
||||
auto& insn = m_instructions[index.row()];
|
||||
|
||||
if (role == GUI::ModelRole::Display) {
|
||||
if (index.column() == Column::Address)
|
||||
return String::formatted("{:p}", insn.address);
|
||||
if (index.column() == Column::InstructionBytes) {
|
||||
StringBuilder builder;
|
||||
for (auto ch : insn.bytes)
|
||||
builder.appendff("{:02x} ", static_cast<unsigned char>(ch));
|
||||
return builder.to_string();
|
||||
}
|
||||
if (index.column() == Column::Disassembly)
|
||||
return insn.disassembly;
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void DisassemblyModel::update()
|
||||
{
|
||||
did_update();
|
||||
}
|
||||
|
||||
}
|
77
Userland/DevTools/HackStudio/Debugger/DisassemblyModel.h
Normal file
77
Userland/DevTools/HackStudio/Debugger/DisassemblyModel.h
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGUI/Model.h>
|
||||
#include <LibX86/Instruction.h>
|
||||
#include <sys/arch/i386/regs.h>
|
||||
|
||||
namespace Debug {
|
||||
|
||||
class DebugSession;
|
||||
|
||||
}
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
struct InstructionData {
|
||||
X86::Instruction insn;
|
||||
String disassembly;
|
||||
StringView bytes;
|
||||
FlatPtr address { 0 };
|
||||
};
|
||||
|
||||
class DisassemblyModel final : public GUI::Model {
|
||||
public:
|
||||
static NonnullRefPtr<DisassemblyModel> create(const Debug::DebugSession& debug_session, const PtraceRegisters& regs)
|
||||
{
|
||||
return adopt(*new DisassemblyModel(debug_session, regs));
|
||||
}
|
||||
|
||||
enum Column {
|
||||
Address,
|
||||
InstructionBytes,
|
||||
Disassembly,
|
||||
__Count
|
||||
};
|
||||
|
||||
virtual ~DisassemblyModel() override;
|
||||
|
||||
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
|
||||
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; }
|
||||
virtual String column_name(int) const override;
|
||||
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
|
||||
virtual void update() override;
|
||||
|
||||
private:
|
||||
DisassemblyModel(const Debug::DebugSession&, const PtraceRegisters&);
|
||||
|
||||
Vector<InstructionData> m_instructions;
|
||||
};
|
||||
|
||||
}
|
105
Userland/DevTools/HackStudio/Debugger/DisassemblyWidget.cpp
Normal file
105
Userland/DevTools/HackStudio/Debugger/DisassemblyWidget.cpp
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "DisassemblyWidget.h"
|
||||
#include "DisassemblyModel.h"
|
||||
#include <LibGUI/BoxLayout.h>
|
||||
#include <LibGUI/Painter.h>
|
||||
#include <LibGfx/Palette.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
void UnavailableDisassemblyWidget::paint_event(GUI::PaintEvent& event)
|
||||
{
|
||||
Frame::paint_event(event);
|
||||
if (reason().is_empty())
|
||||
return;
|
||||
GUI::Painter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
painter.draw_text(frame_inner_rect(), reason(), Gfx::TextAlignment::Center, palette().window_text(), Gfx::TextElision::Right);
|
||||
}
|
||||
|
||||
DisassemblyWidget::DisassemblyWidget()
|
||||
{
|
||||
set_layout<GUI::VerticalBoxLayout>();
|
||||
|
||||
m_top_container = add<GUI::Widget>();
|
||||
m_top_container->set_layout<GUI::HorizontalBoxLayout>();
|
||||
m_top_container->set_fixed_height(20);
|
||||
|
||||
m_function_name_label = m_top_container->add<GUI::Label>("");
|
||||
|
||||
m_disassembly_view = add<GUI::TableView>();
|
||||
|
||||
m_unavailable_disassembly_widget = add<UnavailableDisassemblyWidget>("");
|
||||
|
||||
hide_disassembly("Program isn't running");
|
||||
}
|
||||
|
||||
void DisassemblyWidget::update_state(const Debug::DebugSession& debug_session, const PtraceRegisters& regs)
|
||||
{
|
||||
m_disassembly_view->set_model(DisassemblyModel::create(debug_session, regs));
|
||||
|
||||
if (m_disassembly_view->model()->row_count() > 0) {
|
||||
auto lib = debug_session.library_at(regs.eip);
|
||||
if (!lib)
|
||||
return;
|
||||
auto containing_function = lib->debug_info->get_containing_function(regs.eip - lib->base_address);
|
||||
if (containing_function.has_value())
|
||||
m_function_name_label->set_text(containing_function.value().name);
|
||||
else
|
||||
m_function_name_label->set_text("<missing>");
|
||||
show_disassembly();
|
||||
} else {
|
||||
hide_disassembly("No disassembly to show for this function");
|
||||
}
|
||||
}
|
||||
|
||||
void DisassemblyWidget::program_stopped()
|
||||
{
|
||||
m_disassembly_view->set_model({});
|
||||
m_function_name_label->set_text("");
|
||||
hide_disassembly("Program isn't running");
|
||||
}
|
||||
|
||||
void DisassemblyWidget::show_disassembly()
|
||||
{
|
||||
m_top_container->set_visible(true);
|
||||
m_disassembly_view->set_visible(true);
|
||||
m_function_name_label->set_visible(true);
|
||||
m_unavailable_disassembly_widget->set_visible(false);
|
||||
}
|
||||
|
||||
void DisassemblyWidget::hide_disassembly(const String& reason)
|
||||
{
|
||||
m_top_container->set_visible(false);
|
||||
m_disassembly_view->set_visible(false);
|
||||
m_function_name_label->set_visible(false);
|
||||
m_unavailable_disassembly_widget->set_visible(true);
|
||||
m_unavailable_disassembly_widget->set_reason(reason);
|
||||
}
|
||||
|
||||
}
|
78
Userland/DevTools/HackStudio/Debugger/DisassemblyWidget.h
Normal file
78
Userland/DevTools/HackStudio/Debugger/DisassemblyWidget.h
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Debugger.h"
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <LibGUI/Label.h>
|
||||
#include <LibGUI/Model.h>
|
||||
#include <LibGUI/TableView.h>
|
||||
#include <LibGUI/Widget.h>
|
||||
#include <sys/arch/i386/regs.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class UnavailableDisassemblyWidget final : public GUI::Frame {
|
||||
C_OBJECT(UnavailableDisassemblyWidget)
|
||||
public:
|
||||
virtual ~UnavailableDisassemblyWidget() override { }
|
||||
|
||||
const String& reason() const { return m_reason; }
|
||||
void set_reason(const String& text) { m_reason = text; }
|
||||
|
||||
private:
|
||||
UnavailableDisassemblyWidget(const String& reason)
|
||||
: m_reason(reason)
|
||||
{
|
||||
}
|
||||
|
||||
virtual void paint_event(GUI::PaintEvent& event) override;
|
||||
|
||||
String m_reason;
|
||||
};
|
||||
|
||||
class DisassemblyWidget final : public GUI::Widget {
|
||||
C_OBJECT(DisassemblyWidget)
|
||||
public:
|
||||
virtual ~DisassemblyWidget() override { }
|
||||
|
||||
void update_state(const Debug::DebugSession&, const PtraceRegisters&);
|
||||
void program_stopped();
|
||||
|
||||
private:
|
||||
DisassemblyWidget();
|
||||
|
||||
void show_disassembly();
|
||||
void hide_disassembly(const String&);
|
||||
|
||||
RefPtr<GUI::Widget> m_top_container;
|
||||
RefPtr<GUI::TableView> m_disassembly_view;
|
||||
RefPtr<GUI::Label> m_function_name_label;
|
||||
RefPtr<UnavailableDisassemblyWidget> m_unavailable_disassembly_widget;
|
||||
};
|
||||
|
||||
}
|
121
Userland/DevTools/HackStudio/Debugger/RegistersModel.cpp
Normal file
121
Userland/DevTools/HackStudio/Debugger/RegistersModel.cpp
Normal file
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "RegistersModel.h"
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
RegistersModel::RegistersModel(const PtraceRegisters& regs)
|
||||
: m_raw_registers(regs)
|
||||
{
|
||||
m_registers.append({ "eax", regs.eax });
|
||||
m_registers.append({ "ebx", regs.ebx });
|
||||
m_registers.append({ "ecx", regs.ecx });
|
||||
m_registers.append({ "edx", regs.edx });
|
||||
m_registers.append({ "esp", regs.esp });
|
||||
m_registers.append({ "ebp", regs.ebp });
|
||||
m_registers.append({ "esi", regs.esi });
|
||||
m_registers.append({ "edi", regs.edi });
|
||||
m_registers.append({ "eip", regs.eip });
|
||||
m_registers.append({ "eflags", regs.eflags });
|
||||
m_registers.append({ "cs", regs.cs });
|
||||
m_registers.append({ "ss", regs.ss });
|
||||
m_registers.append({ "ds", regs.ds });
|
||||
m_registers.append({ "es", regs.es });
|
||||
m_registers.append({ "fs", regs.fs });
|
||||
m_registers.append({ "gs", regs.gs });
|
||||
}
|
||||
|
||||
RegistersModel::RegistersModel(const PtraceRegisters& current_regs, const PtraceRegisters& previous_regs)
|
||||
: m_raw_registers(current_regs)
|
||||
{
|
||||
m_registers.append({ "eax", current_regs.eax, current_regs.eax != previous_regs.eax });
|
||||
m_registers.append({ "ebx", current_regs.ebx, current_regs.ebx != previous_regs.ebx });
|
||||
m_registers.append({ "ecx", current_regs.ecx, current_regs.ecx != previous_regs.ecx });
|
||||
m_registers.append({ "edx", current_regs.edx, current_regs.edx != previous_regs.edx });
|
||||
m_registers.append({ "esp", current_regs.esp, current_regs.esp != previous_regs.esp });
|
||||
m_registers.append({ "ebp", current_regs.ebp, current_regs.ebp != previous_regs.ebp });
|
||||
m_registers.append({ "esi", current_regs.esi, current_regs.esi != previous_regs.esi });
|
||||
m_registers.append({ "edi", current_regs.edi, current_regs.edi != previous_regs.edi });
|
||||
m_registers.append({ "eip", current_regs.eip, current_regs.eip != previous_regs.eip });
|
||||
m_registers.append({ "eflags", current_regs.eflags, current_regs.eflags != previous_regs.eflags });
|
||||
m_registers.append({ "cs", current_regs.cs, current_regs.cs != previous_regs.cs });
|
||||
m_registers.append({ "ss", current_regs.ss, current_regs.ss != previous_regs.ss });
|
||||
m_registers.append({ "ds", current_regs.ds, current_regs.ds != previous_regs.ds });
|
||||
m_registers.append({ "es", current_regs.es, current_regs.es != previous_regs.es });
|
||||
m_registers.append({ "fs", current_regs.fs, current_regs.ds != previous_regs.fs });
|
||||
m_registers.append({ "gs", current_regs.gs, current_regs.gs != previous_regs.gs });
|
||||
}
|
||||
|
||||
RegistersModel::~RegistersModel()
|
||||
{
|
||||
}
|
||||
|
||||
int RegistersModel::row_count(const GUI::ModelIndex&) const
|
||||
{
|
||||
return m_registers.size();
|
||||
}
|
||||
|
||||
String RegistersModel::column_name(int column) const
|
||||
{
|
||||
switch (column) {
|
||||
case Column::Register:
|
||||
return "Register";
|
||||
case Column::Value:
|
||||
return "Value";
|
||||
default:
|
||||
ASSERT_NOT_REACHED();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
GUI::Variant RegistersModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
|
||||
{
|
||||
auto& reg = m_registers[index.row()];
|
||||
|
||||
if (role == GUI::ModelRole::ForegroundColor) {
|
||||
if (reg.changed)
|
||||
return Color(Color::Red);
|
||||
else
|
||||
return Color(Color::Black);
|
||||
}
|
||||
|
||||
if (role == GUI::ModelRole::Display) {
|
||||
if (index.column() == Column::Register)
|
||||
return reg.name;
|
||||
if (index.column() == Column::Value)
|
||||
return String::formatted("{:08x}", reg.value);
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void RegistersModel::update()
|
||||
{
|
||||
did_update();
|
||||
}
|
||||
|
||||
}
|
77
Userland/DevTools/HackStudio/Debugger/RegistersModel.h
Normal file
77
Userland/DevTools/HackStudio/Debugger/RegistersModel.h
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGUI/Model.h>
|
||||
#include <sys/arch/i386/regs.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
struct RegisterData {
|
||||
String name;
|
||||
u32 value;
|
||||
bool changed { false };
|
||||
};
|
||||
|
||||
class RegistersModel final : public GUI::Model {
|
||||
public:
|
||||
static RefPtr<RegistersModel> create(const PtraceRegisters& regs)
|
||||
{
|
||||
return adopt(*new RegistersModel(regs));
|
||||
}
|
||||
|
||||
static RefPtr<RegistersModel> create(const PtraceRegisters& current_regs, const PtraceRegisters& previous_regs)
|
||||
{
|
||||
return adopt(*new RegistersModel(current_regs, previous_regs));
|
||||
}
|
||||
|
||||
enum Column {
|
||||
Register,
|
||||
Value,
|
||||
__Count
|
||||
};
|
||||
|
||||
virtual ~RegistersModel() override;
|
||||
|
||||
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
|
||||
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; }
|
||||
virtual String column_name(int) const override;
|
||||
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
|
||||
virtual void update() override;
|
||||
|
||||
const PtraceRegisters& raw_registers() const { return m_raw_registers; }
|
||||
|
||||
private:
|
||||
explicit RegistersModel(const PtraceRegisters& regs);
|
||||
RegistersModel(const PtraceRegisters& current_regs, const PtraceRegisters& previous_regs);
|
||||
|
||||
PtraceRegisters m_raw_registers;
|
||||
Vector<RegisterData> m_registers;
|
||||
};
|
||||
|
||||
}
|
194
Userland/DevTools/HackStudio/Debugger/VariablesModel.cpp
Normal file
194
Userland/DevTools/HackStudio/Debugger/VariablesModel.cpp
Normal file
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "VariablesModel.h"
|
||||
#include <LibGUI/Application.h>
|
||||
#include <LibGUI/MessageBox.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
GUI::ModelIndex VariablesModel::index(int row, int column, const GUI::ModelIndex& parent_index) const
|
||||
{
|
||||
if (!parent_index.is_valid())
|
||||
return create_index(row, column, &m_variables[row]);
|
||||
auto* parent = static_cast<const Debug::DebugInfo::VariableInfo*>(parent_index.internal_data());
|
||||
auto* child = &parent->members[row];
|
||||
return create_index(row, column, child);
|
||||
}
|
||||
|
||||
GUI::ModelIndex VariablesModel::parent_index(const GUI::ModelIndex& index) const
|
||||
{
|
||||
if (!index.is_valid())
|
||||
return {};
|
||||
auto* child = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data());
|
||||
auto* parent = child->parent;
|
||||
if (parent == nullptr)
|
||||
return {};
|
||||
|
||||
if (parent->parent == nullptr) {
|
||||
for (size_t row = 0; row < m_variables.size(); row++)
|
||||
if (m_variables.ptr_at(row).ptr() == parent)
|
||||
return create_index(row, 0, parent);
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
for (size_t row = 0; row < parent->parent->members.size(); row++) {
|
||||
Debug::DebugInfo::VariableInfo* child_at_row = parent->parent->members.ptr_at(row).ptr();
|
||||
if (child_at_row == parent)
|
||||
return create_index(row, 0, parent);
|
||||
}
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
int VariablesModel::row_count(const GUI::ModelIndex& index) const
|
||||
{
|
||||
if (!index.is_valid())
|
||||
return m_variables.size();
|
||||
auto* node = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data());
|
||||
return node->members.size();
|
||||
}
|
||||
|
||||
static String variable_value_as_string(const Debug::DebugInfo::VariableInfo& variable)
|
||||
{
|
||||
if (variable.location_type != Debug::DebugInfo::VariableInfo::LocationType::Address)
|
||||
return "N/A";
|
||||
|
||||
auto variable_address = variable.location_data.address;
|
||||
|
||||
if (variable.is_enum_type()) {
|
||||
auto value = Debugger::the().session()->peek((u32*)variable_address);
|
||||
ASSERT(value.has_value());
|
||||
auto it = variable.type->members.find_if([&enumerator_value = value.value()](const auto& enumerator) {
|
||||
return enumerator->constant_data.as_u32 == enumerator_value;
|
||||
});
|
||||
ASSERT(!it.is_end());
|
||||
return String::formatted("{}::{}", variable.type_name, (*it)->name);
|
||||
}
|
||||
|
||||
if (variable.type_name == "int") {
|
||||
auto value = Debugger::the().session()->peek((u32*)variable_address);
|
||||
ASSERT(value.has_value());
|
||||
return String::formatted("{}", static_cast<int>(value.value()));
|
||||
}
|
||||
|
||||
if (variable.type_name == "char") {
|
||||
auto value = Debugger::the().session()->peek((u32*)variable_address);
|
||||
ASSERT(value.has_value());
|
||||
return String::formatted("'{0:c}' ({0:d})", value.value());
|
||||
}
|
||||
|
||||
if (variable.type_name == "bool") {
|
||||
auto value = Debugger::the().session()->peek((u32*)variable_address);
|
||||
ASSERT(value.has_value());
|
||||
return (value.value() & 1) ? "true" : "false";
|
||||
}
|
||||
|
||||
return String::formatted("type: {} @ {:p}, ", variable.type_name, variable_address);
|
||||
}
|
||||
|
||||
static Optional<u32> string_to_variable_value(const StringView& string_value, const Debug::DebugInfo::VariableInfo& variable)
|
||||
{
|
||||
if (variable.is_enum_type()) {
|
||||
auto prefix_string = String::formatted("{}::", variable.type_name);
|
||||
auto string_to_use = string_value;
|
||||
if (string_value.starts_with(prefix_string))
|
||||
string_to_use = string_value.substring_view(prefix_string.length(), string_value.length() - prefix_string.length());
|
||||
|
||||
auto it = variable.type->members.find_if([string_to_use](const auto& enumerator) {
|
||||
return enumerator->name == string_to_use;
|
||||
});
|
||||
|
||||
if (it.is_end())
|
||||
return {};
|
||||
return (*it)->constant_data.as_u32;
|
||||
}
|
||||
|
||||
if (variable.type_name == "int") {
|
||||
auto value = string_value.to_int();
|
||||
if (value.has_value())
|
||||
return value.value();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (variable.type_name == "bool") {
|
||||
if (string_value == "true")
|
||||
return true;
|
||||
if (string_value == "false")
|
||||
return false;
|
||||
return {};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void VariablesModel::set_variable_value(const GUI::ModelIndex& index, const StringView& string_value, GUI::Window* parent_window)
|
||||
{
|
||||
auto variable = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data());
|
||||
|
||||
auto value = string_to_variable_value(string_value, *variable);
|
||||
|
||||
if (value.has_value()) {
|
||||
auto success = Debugger::the().session()->poke((u32*)variable->location_data.address, value.value());
|
||||
ASSERT(success);
|
||||
return;
|
||||
}
|
||||
|
||||
GUI::MessageBox::show(
|
||||
parent_window,
|
||||
String::formatted("String value \"{}\" could not be converted to a value of type {}.", string_value, variable->type_name),
|
||||
"Set value failed",
|
||||
GUI::MessageBox::Type::Error);
|
||||
}
|
||||
|
||||
GUI::Variant VariablesModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
|
||||
{
|
||||
auto* variable = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data());
|
||||
switch (role) {
|
||||
case GUI::ModelRole::Display: {
|
||||
auto value_as_string = variable_value_as_string(*variable);
|
||||
return String::formatted("{}: {}", variable->name, value_as_string);
|
||||
}
|
||||
case GUI::ModelRole::Icon:
|
||||
return m_variable_icon;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
void VariablesModel::update()
|
||||
{
|
||||
did_update();
|
||||
}
|
||||
|
||||
RefPtr<VariablesModel> VariablesModel::create(const PtraceRegisters& regs)
|
||||
{
|
||||
auto lib = Debugger::the().session()->library_at(regs.eip);
|
||||
if (!lib)
|
||||
return nullptr;
|
||||
auto variables = lib->debug_info->get_variables_in_current_scope(regs);
|
||||
return adopt(*new VariablesModel(move(variables), regs));
|
||||
}
|
||||
|
||||
}
|
63
Userland/DevTools/HackStudio/Debugger/VariablesModel.h
Normal file
63
Userland/DevTools/HackStudio/Debugger/VariablesModel.h
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Debugger.h"
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <LibGUI/Model.h>
|
||||
#include <LibGUI/TreeView.h>
|
||||
#include <sys/arch/i386/regs.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class VariablesModel final : public GUI::Model {
|
||||
public:
|
||||
static RefPtr<VariablesModel> create(const PtraceRegisters& regs);
|
||||
|
||||
void set_variable_value(const GUI::ModelIndex&, const StringView&, GUI::Window*);
|
||||
|
||||
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
|
||||
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return 1; }
|
||||
virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override;
|
||||
virtual void update() override;
|
||||
virtual GUI::ModelIndex parent_index(const GUI::ModelIndex&) const override;
|
||||
virtual GUI::ModelIndex index(int row, int column = 0, const GUI::ModelIndex& = GUI::ModelIndex()) const override;
|
||||
|
||||
private:
|
||||
explicit VariablesModel(NonnullOwnPtrVector<Debug::DebugInfo::VariableInfo>&& variables, const PtraceRegisters& regs)
|
||||
: m_variables(move(variables))
|
||||
, m_regs(regs)
|
||||
{
|
||||
m_variable_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/inspector-object.png"));
|
||||
}
|
||||
NonnullOwnPtrVector<Debug::DebugInfo::VariableInfo> m_variables;
|
||||
PtraceRegisters m_regs;
|
||||
|
||||
GUI::Icon m_variable_icon;
|
||||
};
|
||||
|
||||
}
|
530
Userland/DevTools/HackStudio/Editor.cpp
Normal file
530
Userland/DevTools/HackStudio/Editor.cpp
Normal file
|
@ -0,0 +1,530 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "Editor.h"
|
||||
#include "Debugger/Debugger.h"
|
||||
#include "EditorWrapper.h"
|
||||
#include "HackStudio.h"
|
||||
#include "Language.h"
|
||||
#include <AK/ByteBuffer.h>
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <LibCore/DirIterator.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibGUI/Application.h>
|
||||
#include <LibGUI/CppSyntaxHighlighter.h>
|
||||
#include <LibGUI/GMLSyntaxHighlighter.h>
|
||||
#include <LibGUI/INISyntaxHighlighter.h>
|
||||
#include <LibGUI/JSSyntaxHighlighter.h>
|
||||
#include <LibGUI/Label.h>
|
||||
#include <LibGUI/Painter.h>
|
||||
#include <LibGUI/ScrollBar.h>
|
||||
#include <LibGUI/ShellSyntaxHighlighter.h>
|
||||
#include <LibGUI/SyntaxHighlighter.h>
|
||||
#include <LibGUI/Window.h>
|
||||
#include <LibMarkdown/Document.h>
|
||||
#include <LibWeb/DOM/ElementFactory.h>
|
||||
#include <LibWeb/DOM/Text.h>
|
||||
#include <LibWeb/HTML/HTMLHeadElement.h>
|
||||
#include <LibWeb/OutOfProcessWebView.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
// #define EDITOR_DEBUG
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
Editor::Editor()
|
||||
{
|
||||
set_document(CodeDocument::create());
|
||||
m_documentation_tooltip_window = GUI::Window::construct();
|
||||
m_documentation_tooltip_window->set_rect(0, 0, 500, 400);
|
||||
m_documentation_tooltip_window->set_window_type(GUI::WindowType::Tooltip);
|
||||
m_documentation_page_view = m_documentation_tooltip_window->set_main_widget<Web::OutOfProcessWebView>();
|
||||
}
|
||||
|
||||
Editor::~Editor()
|
||||
{
|
||||
}
|
||||
|
||||
EditorWrapper& Editor::wrapper()
|
||||
{
|
||||
return static_cast<EditorWrapper&>(*parent());
|
||||
}
|
||||
const EditorWrapper& Editor::wrapper() const
|
||||
{
|
||||
return static_cast<const EditorWrapper&>(*parent());
|
||||
}
|
||||
|
||||
void Editor::focusin_event(GUI::FocusEvent& event)
|
||||
{
|
||||
wrapper().set_editor_has_focus({}, true);
|
||||
if (on_focus)
|
||||
on_focus();
|
||||
GUI::TextEditor::focusin_event(event);
|
||||
}
|
||||
|
||||
void Editor::focusout_event(GUI::FocusEvent& event)
|
||||
{
|
||||
wrapper().set_editor_has_focus({}, false);
|
||||
GUI::TextEditor::focusout_event(event);
|
||||
}
|
||||
|
||||
Gfx::IntRect Editor::breakpoint_icon_rect(size_t line_number) const
|
||||
{
|
||||
auto ruler_line_rect = ruler_content_rect(line_number);
|
||||
|
||||
auto scroll_value = vertical_scrollbar().value();
|
||||
ruler_line_rect = ruler_line_rect.translated({ 0, -scroll_value });
|
||||
auto center = ruler_line_rect.center().translated({ ruler_line_rect.width() - 10, -line_spacing() - 3 });
|
||||
constexpr int size = 32;
|
||||
return { center.x() - size / 2, center.y() - size / 2, size, size };
|
||||
}
|
||||
|
||||
void Editor::paint_event(GUI::PaintEvent& event)
|
||||
{
|
||||
GUI::TextEditor::paint_event(event);
|
||||
|
||||
GUI::Painter painter(*this);
|
||||
if (is_focused()) {
|
||||
painter.add_clip_rect(event.rect());
|
||||
|
||||
auto rect = frame_inner_rect();
|
||||
if (vertical_scrollbar().is_visible())
|
||||
rect.set_width(rect.width() - vertical_scrollbar().width());
|
||||
if (horizontal_scrollbar().is_visible())
|
||||
rect.set_height(rect.height() - horizontal_scrollbar().height());
|
||||
painter.draw_rect(rect, palette().selection());
|
||||
}
|
||||
|
||||
if (ruler_visible()) {
|
||||
size_t first_visible_line = text_position_at(event.rect().top_left()).line();
|
||||
size_t last_visible_line = text_position_at(event.rect().bottom_right()).line();
|
||||
for (size_t line : breakpoint_lines()) {
|
||||
if (line < first_visible_line || line > last_visible_line) {
|
||||
continue;
|
||||
}
|
||||
const auto& icon = breakpoint_icon_bitmap();
|
||||
painter.blit(breakpoint_icon_rect(line).center(), icon, icon.rect());
|
||||
}
|
||||
if (execution_position().has_value()) {
|
||||
const auto& icon = current_position_icon_bitmap();
|
||||
painter.blit(breakpoint_icon_rect(execution_position().value()).center(), icon, icon.rect());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static HashMap<String, String>& man_paths()
|
||||
{
|
||||
static HashMap<String, String> paths;
|
||||
if (paths.is_empty()) {
|
||||
// FIXME: This should also search man3, possibly other places..
|
||||
Core::DirIterator it("/usr/share/man/man2", Core::DirIterator::Flags::SkipDots);
|
||||
while (it.has_next()) {
|
||||
auto path = String::formatted("/usr/share/man/man2/{}", it.next_path());
|
||||
auto title = LexicalPath(path).title();
|
||||
paths.set(title, path);
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
void Editor::show_documentation_tooltip_if_available(const String& hovered_token, const Gfx::IntPoint& screen_location)
|
||||
{
|
||||
auto it = man_paths().find(hovered_token);
|
||||
if (it == man_paths().end()) {
|
||||
#ifdef EDITOR_DEBUG
|
||||
dbgln("no man path for {}", hovered_token);
|
||||
#endif
|
||||
m_documentation_tooltip_window->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_documentation_tooltip_window->is_visible() && hovered_token == m_last_parsed_token) {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef EDITOR_DEBUG
|
||||
dbgln("opening {}", it->value);
|
||||
#endif
|
||||
auto file = Core::File::construct(it->value);
|
||||
if (!file->open(Core::File::ReadOnly)) {
|
||||
dbgln("failed to open {}, {}", it->value, file->error_string());
|
||||
return;
|
||||
}
|
||||
|
||||
auto man_document = Markdown::Document::parse(file->read_all());
|
||||
|
||||
if (!man_document) {
|
||||
dbgln("failed to parse markdown");
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder html;
|
||||
// FIXME: With the InProcessWebView we used to manipulate the document body directly,
|
||||
// With OutOfProcessWebView this isn't possible (at the moment). The ideal solution
|
||||
// is probably to tweak Markdown::Document::render_to_html() so we can inject styles
|
||||
// into the rendered HTML easily.
|
||||
html.append(man_document->render_to_html());
|
||||
html.append("<style>body { background-color: #dac7b5; }</style>");
|
||||
m_documentation_page_view->load_html(html.build(), {});
|
||||
|
||||
m_documentation_tooltip_window->move_to(screen_location.translated(4, 4));
|
||||
m_documentation_tooltip_window->show();
|
||||
|
||||
m_last_parsed_token = hovered_token;
|
||||
}
|
||||
|
||||
void Editor::mousemove_event(GUI::MouseEvent& event)
|
||||
{
|
||||
GUI::TextEditor::mousemove_event(event);
|
||||
|
||||
if (document().spans().is_empty())
|
||||
return;
|
||||
|
||||
auto text_position = text_position_at(event.position());
|
||||
if (!text_position.is_valid()) {
|
||||
m_documentation_tooltip_window->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
auto highlighter = wrapper().editor().syntax_highlighter();
|
||||
if (!highlighter)
|
||||
return;
|
||||
|
||||
bool hide_tooltip = true;
|
||||
bool is_over_link = false;
|
||||
|
||||
auto ruler_line_rect = ruler_content_rect(text_position.line());
|
||||
auto hovering_lines_ruler = (event.position().x() < ruler_line_rect.width());
|
||||
if (hovering_lines_ruler && !is_in_drag_select())
|
||||
set_override_cursor(Gfx::StandardCursor::Arrow);
|
||||
else if (m_hovering_editor)
|
||||
set_override_cursor(m_hovering_link && event.ctrl() ? Gfx::StandardCursor::Hand : Gfx::StandardCursor::IBeam);
|
||||
|
||||
for (auto& span : document().spans()) {
|
||||
if (span.range.contains(m_previous_text_position) && !span.range.contains(text_position)) {
|
||||
if (highlighter->is_navigatable(span.data) && span.attributes.underline) {
|
||||
span.attributes.underline = false;
|
||||
wrapper().editor().update();
|
||||
}
|
||||
}
|
||||
|
||||
if (span.range.contains(text_position)) {
|
||||
auto adjusted_range = span.range;
|
||||
auto end_line_length = document().line(span.range.end().line()).length();
|
||||
adjusted_range.end().set_column(min(end_line_length, adjusted_range.end().column() + 1));
|
||||
auto hovered_span_text = document().text_in_range(adjusted_range);
|
||||
#ifdef EDITOR_DEBUG
|
||||
dbgln("Hovering: {} \"{}\"", adjusted_range, hovered_span_text);
|
||||
#endif
|
||||
|
||||
if (highlighter->is_navigatable(span.data)) {
|
||||
is_over_link = true;
|
||||
bool was_underlined = span.attributes.underline;
|
||||
span.attributes.underline = event.modifiers() & Mod_Ctrl;
|
||||
if (span.attributes.underline != was_underlined) {
|
||||
wrapper().editor().update();
|
||||
}
|
||||
}
|
||||
if (highlighter->is_identifier(span.data)) {
|
||||
show_documentation_tooltip_if_available(hovered_span_text, event.position().translated(screen_relative_rect().location()));
|
||||
hide_tooltip = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_previous_text_position = text_position;
|
||||
if (hide_tooltip)
|
||||
m_documentation_tooltip_window->hide();
|
||||
|
||||
m_hovering_link = is_over_link && (event.modifiers() & Mod_Ctrl);
|
||||
}
|
||||
|
||||
void Editor::mousedown_event(GUI::MouseEvent& event)
|
||||
{
|
||||
auto highlighter = wrapper().editor().syntax_highlighter();
|
||||
if (!highlighter) {
|
||||
GUI::TextEditor::mousedown_event(event);
|
||||
return;
|
||||
}
|
||||
|
||||
auto text_position = text_position_at(event.position());
|
||||
auto ruler_line_rect = ruler_content_rect(text_position.line());
|
||||
if (event.button() == GUI::MouseButton::Left && event.position().x() < ruler_line_rect.width()) {
|
||||
if (!breakpoint_lines().contains_slow(text_position.line())) {
|
||||
breakpoint_lines().append(text_position.line());
|
||||
Debugger::on_breakpoint_change(wrapper().filename_label().text(), text_position.line(), BreakpointChange::Added);
|
||||
} else {
|
||||
breakpoint_lines().remove_first_matching([&](size_t line) { return line == text_position.line(); });
|
||||
Debugger::on_breakpoint_change(wrapper().filename_label().text(), text_position.line(), BreakpointChange::Removed);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(event.modifiers() & Mod_Ctrl)) {
|
||||
GUI::TextEditor::mousedown_event(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!text_position.is_valid()) {
|
||||
GUI::TextEditor::mousedown_event(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto* span = document().span_at(text_position)) {
|
||||
if (!highlighter->is_navigatable(span->data)) {
|
||||
GUI::TextEditor::mousedown_event(event);
|
||||
return;
|
||||
}
|
||||
|
||||
auto adjusted_range = span->range;
|
||||
adjusted_range.end().set_column(adjusted_range.end().column() + 1);
|
||||
auto span_text = document().text_in_range(adjusted_range);
|
||||
auto header_path = span_text.substring(1, span_text.length() - 2);
|
||||
#ifdef EDITOR_DEBUG
|
||||
dbgln("Ctrl+click: {} \"{}\"", adjusted_range, header_path);
|
||||
#endif
|
||||
navigate_to_include_if_available(header_path);
|
||||
return;
|
||||
}
|
||||
|
||||
GUI::TextEditor::mousedown_event(event);
|
||||
}
|
||||
|
||||
void Editor::enter_event(Core::Event& event)
|
||||
{
|
||||
m_hovering_editor = true;
|
||||
GUI::TextEditor::enter_event(event);
|
||||
}
|
||||
|
||||
void Editor::leave_event(Core::Event& event)
|
||||
{
|
||||
m_hovering_editor = false;
|
||||
GUI::TextEditor::leave_event(event);
|
||||
}
|
||||
|
||||
static HashMap<String, String>& include_paths()
|
||||
{
|
||||
static HashMap<String, String> paths;
|
||||
|
||||
auto add_directory = [](String base, Optional<String> recursive, auto handle_directory) -> void {
|
||||
Core::DirIterator it(recursive.value_or(base), Core::DirIterator::Flags::SkipDots);
|
||||
while (it.has_next()) {
|
||||
auto path = it.next_full_path();
|
||||
if (!Core::File::is_directory(path)) {
|
||||
auto key = path.substring(base.length() + 1, path.length() - base.length() - 1);
|
||||
#ifdef EDITOR_DEBUG
|
||||
dbgln("Adding header \"{}\" in path \"{}\"", key, path);
|
||||
#endif
|
||||
paths.set(key, path);
|
||||
} else {
|
||||
handle_directory(base, path, handle_directory);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (paths.is_empty()) {
|
||||
add_directory(".", {}, add_directory);
|
||||
add_directory("/usr/local/include", {}, add_directory);
|
||||
add_directory("/usr/local/include/c++/9.2.0", {}, add_directory);
|
||||
add_directory("/usr/include", {}, add_directory);
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
void Editor::navigate_to_include_if_available(String path)
|
||||
{
|
||||
auto it = include_paths().find(path);
|
||||
if (it == include_paths().end()) {
|
||||
#ifdef EDITOR_DEBUG
|
||||
dbgln("no header {} found.", path);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
on_open(it->value);
|
||||
}
|
||||
|
||||
void Editor::set_execution_position(size_t line_number)
|
||||
{
|
||||
code_document().set_execution_position(line_number);
|
||||
scroll_position_into_view({ line_number, 0 });
|
||||
update(breakpoint_icon_rect(line_number));
|
||||
}
|
||||
|
||||
void Editor::clear_execution_position()
|
||||
{
|
||||
if (!execution_position().has_value()) {
|
||||
return;
|
||||
}
|
||||
size_t previous_position = execution_position().value();
|
||||
code_document().clear_execution_position();
|
||||
update(breakpoint_icon_rect(previous_position));
|
||||
}
|
||||
|
||||
const Gfx::Bitmap& Editor::breakpoint_icon_bitmap()
|
||||
{
|
||||
static auto bitmap = Gfx::Bitmap::load_from_file("/res/icons/16x16/breakpoint.png");
|
||||
return *bitmap;
|
||||
}
|
||||
|
||||
const Gfx::Bitmap& Editor::current_position_icon_bitmap()
|
||||
{
|
||||
static auto bitmap = Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png");
|
||||
return *bitmap;
|
||||
}
|
||||
|
||||
const CodeDocument& Editor::code_document() const
|
||||
{
|
||||
const auto& doc = document();
|
||||
ASSERT(doc.is_code_document());
|
||||
return static_cast<const CodeDocument&>(doc);
|
||||
}
|
||||
|
||||
CodeDocument& Editor::code_document()
|
||||
{
|
||||
return const_cast<CodeDocument&>(static_cast<const Editor&>(*this).code_document());
|
||||
}
|
||||
|
||||
void Editor::set_document(GUI::TextDocument& doc)
|
||||
{
|
||||
ASSERT(doc.is_code_document());
|
||||
GUI::TextEditor::set_document(doc);
|
||||
|
||||
set_override_cursor(Gfx::StandardCursor::IBeam);
|
||||
|
||||
CodeDocument& code_document = static_cast<CodeDocument&>(doc);
|
||||
switch (code_document.language()) {
|
||||
case Language::Cpp:
|
||||
set_syntax_highlighter(make<GUI::CppSyntaxHighlighter>());
|
||||
m_language_client = get_language_client<LanguageClients::Cpp::ServerConnection>(project().root_path());
|
||||
break;
|
||||
case Language::GML:
|
||||
set_syntax_highlighter(make<GUI::GMLSyntaxHighlighter>());
|
||||
break;
|
||||
case Language::JavaScript:
|
||||
set_syntax_highlighter(make<GUI::JSSyntaxHighlighter>());
|
||||
break;
|
||||
case Language::Ini:
|
||||
set_syntax_highlighter(make<GUI::IniSyntaxHighlighter>());
|
||||
break;
|
||||
case Language::Shell:
|
||||
set_syntax_highlighter(make<GUI::ShellSyntaxHighlighter>());
|
||||
m_language_client = get_language_client<LanguageClients::Shell::ServerConnection>(project().root_path());
|
||||
break;
|
||||
default:
|
||||
set_syntax_highlighter({});
|
||||
}
|
||||
|
||||
if (m_language_client) {
|
||||
set_autocomplete_provider(make<LanguageServerAidedAutocompleteProvider>(*m_language_client));
|
||||
dbgln("Opening {}", code_document.file_path());
|
||||
int fd = open(code_document.file_path().characters(), O_RDONLY | O_NOCTTY);
|
||||
if (fd < 0) {
|
||||
perror("open");
|
||||
return;
|
||||
}
|
||||
m_language_client->open_file(code_document.file_path(), fd);
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
Optional<Editor::AutoCompleteRequestData> Editor::get_autocomplete_request_data()
|
||||
{
|
||||
if (!wrapper().editor().m_language_client)
|
||||
return {};
|
||||
|
||||
return Editor::AutoCompleteRequestData { cursor() };
|
||||
}
|
||||
|
||||
void Editor::LanguageServerAidedAutocompleteProvider::provide_completions(Function<void(Vector<Entry>)> callback)
|
||||
{
|
||||
auto& editor = static_cast<Editor&>(*m_editor).wrapper().editor();
|
||||
auto data = editor.get_autocomplete_request_data();
|
||||
if (!data.has_value())
|
||||
callback({});
|
||||
|
||||
m_language_client.on_autocomplete_suggestions = [callback = move(callback)](auto suggestions) {
|
||||
callback(suggestions);
|
||||
};
|
||||
|
||||
m_language_client.request_autocomplete(
|
||||
editor.code_document().file_path(),
|
||||
data.value().position.line(),
|
||||
data.value().position.column());
|
||||
}
|
||||
|
||||
void Editor::on_edit_action(const GUI::Command& command)
|
||||
{
|
||||
if (!m_language_client)
|
||||
return;
|
||||
|
||||
if (command.is_insert_text()) {
|
||||
const GUI::InsertTextCommand& insert_command = static_cast<const GUI::InsertTextCommand&>(command);
|
||||
m_language_client->insert_text(
|
||||
code_document().file_path(),
|
||||
insert_command.text(),
|
||||
insert_command.range().start().line(),
|
||||
insert_command.range().start().column());
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.is_remove_text()) {
|
||||
const GUI::RemoveTextCommand& remove_command = static_cast<const GUI::RemoveTextCommand&>(command);
|
||||
m_language_client->remove_text(
|
||||
code_document().file_path(),
|
||||
remove_command.range().start().line(),
|
||||
remove_command.range().start().column(),
|
||||
remove_command.range().end().line(),
|
||||
remove_command.range().end().column());
|
||||
return;
|
||||
}
|
||||
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
void Editor::undo()
|
||||
{
|
||||
TextEditor::undo();
|
||||
flush_file_content_to_langauge_server();
|
||||
}
|
||||
|
||||
void Editor::redo()
|
||||
{
|
||||
TextEditor::redo();
|
||||
flush_file_content_to_langauge_server();
|
||||
}
|
||||
|
||||
void Editor::flush_file_content_to_langauge_server()
|
||||
{
|
||||
if (!m_language_client)
|
||||
return;
|
||||
|
||||
m_language_client->set_file_content(
|
||||
code_document().file_path(),
|
||||
document().text());
|
||||
}
|
||||
}
|
118
Userland/DevTools/HackStudio/Editor.h
Normal file
118
Userland/DevTools/HackStudio/Editor.h
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CodeDocument.h"
|
||||
#include "Debugger/BreakpointCallback.h"
|
||||
#include "LanguageClient.h"
|
||||
#include <AK/Optional.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <LibGUI/TextEditor.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class EditorWrapper;
|
||||
|
||||
class Editor final : public GUI::TextEditor {
|
||||
C_OBJECT(Editor)
|
||||
public:
|
||||
virtual ~Editor() override;
|
||||
|
||||
Function<void()> on_focus;
|
||||
Function<void(String)> on_open;
|
||||
|
||||
EditorWrapper& wrapper();
|
||||
const EditorWrapper& wrapper() const;
|
||||
|
||||
const Vector<size_t>& breakpoint_lines() const { return code_document().breakpoint_lines(); }
|
||||
Vector<size_t>& breakpoint_lines() { return code_document().breakpoint_lines(); }
|
||||
Optional<size_t> execution_position() const { return code_document().execution_position(); }
|
||||
void set_execution_position(size_t line_number);
|
||||
void clear_execution_position();
|
||||
|
||||
const CodeDocument& code_document() const;
|
||||
CodeDocument& code_document();
|
||||
|
||||
virtual void set_document(GUI::TextDocument&) override;
|
||||
|
||||
virtual void on_edit_action(const GUI::Command&) override;
|
||||
|
||||
virtual void undo() override;
|
||||
virtual void redo() override;
|
||||
|
||||
private:
|
||||
virtual void focusin_event(GUI::FocusEvent&) override;
|
||||
virtual void focusout_event(GUI::FocusEvent&) override;
|
||||
virtual void paint_event(GUI::PaintEvent&) override;
|
||||
virtual void mousemove_event(GUI::MouseEvent&) override;
|
||||
virtual void mousedown_event(GUI::MouseEvent&) override;
|
||||
virtual void enter_event(Core::Event&) override;
|
||||
virtual void leave_event(Core::Event&) override;
|
||||
|
||||
void show_documentation_tooltip_if_available(const String&, const Gfx::IntPoint& screen_location);
|
||||
void navigate_to_include_if_available(String);
|
||||
|
||||
Gfx::IntRect breakpoint_icon_rect(size_t line_number) const;
|
||||
static const Gfx::Bitmap& breakpoint_icon_bitmap();
|
||||
static const Gfx::Bitmap& current_position_icon_bitmap();
|
||||
|
||||
struct AutoCompleteRequestData {
|
||||
GUI::TextPosition position;
|
||||
};
|
||||
|
||||
class LanguageServerAidedAutocompleteProvider final : virtual public GUI::AutocompleteProvider {
|
||||
public:
|
||||
LanguageServerAidedAutocompleteProvider(LanguageClient& client)
|
||||
: m_language_client(client)
|
||||
{
|
||||
}
|
||||
virtual ~LanguageServerAidedAutocompleteProvider() override { }
|
||||
|
||||
private:
|
||||
virtual void provide_completions(Function<void(Vector<Entry>)> callback) override;
|
||||
LanguageClient& m_language_client;
|
||||
};
|
||||
|
||||
Optional<AutoCompleteRequestData> get_autocomplete_request_data();
|
||||
|
||||
void flush_file_content_to_langauge_server();
|
||||
|
||||
explicit Editor();
|
||||
|
||||
RefPtr<GUI::Window> m_documentation_tooltip_window;
|
||||
RefPtr<Web::OutOfProcessWebView> m_documentation_page_view;
|
||||
String m_last_parsed_token;
|
||||
GUI::TextPosition m_previous_text_position { 0, 0 };
|
||||
bool m_hovering_editor { false };
|
||||
bool m_hovering_link { false };
|
||||
bool m_autocomplete_in_focus { false };
|
||||
|
||||
OwnPtr<LanguageClient> m_language_client;
|
||||
};
|
||||
|
||||
}
|
84
Userland/DevTools/HackStudio/EditorWrapper.cpp
Normal file
84
Userland/DevTools/HackStudio/EditorWrapper.cpp
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "EditorWrapper.h"
|
||||
#include "Editor.h"
|
||||
#include "HackStudio.h"
|
||||
#include <LibGUI/Action.h>
|
||||
#include <LibGUI/BoxLayout.h>
|
||||
#include <LibGUI/InputBox.h>
|
||||
#include <LibGUI/Label.h>
|
||||
#include <LibGfx/Font.h>
|
||||
#include <LibGfx/FontDatabase.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
EditorWrapper::EditorWrapper()
|
||||
{
|
||||
set_layout<GUI::VerticalBoxLayout>();
|
||||
|
||||
auto& label_wrapper = add<GUI::Widget>();
|
||||
label_wrapper.set_fixed_height(14);
|
||||
label_wrapper.set_fill_with_background_color(true);
|
||||
label_wrapper.set_layout<GUI::HorizontalBoxLayout>();
|
||||
label_wrapper.layout()->set_margins({ 2, 0, 2, 0 });
|
||||
|
||||
m_filename_label = label_wrapper.add<GUI::Label>("(Untitled)");
|
||||
m_filename_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
|
||||
m_filename_label->set_fixed_height(14);
|
||||
|
||||
m_cursor_label = label_wrapper.add<GUI::Label>("(Cursor)");
|
||||
m_cursor_label->set_text_alignment(Gfx::TextAlignment::CenterRight);
|
||||
m_cursor_label->set_fixed_height(14);
|
||||
|
||||
m_editor = add<Editor>();
|
||||
m_editor->set_ruler_visible(true);
|
||||
m_editor->set_line_wrapping_enabled(true);
|
||||
m_editor->set_automatic_indentation_enabled(true);
|
||||
|
||||
m_editor->on_cursor_change = [this] {
|
||||
m_cursor_label->set_text(String::formatted("Line: {}, Column: {}", m_editor->cursor().line() + 1, m_editor->cursor().column()));
|
||||
};
|
||||
|
||||
m_editor->on_focus = [this] {
|
||||
set_current_editor_wrapper(this);
|
||||
};
|
||||
|
||||
m_editor->on_open = [](String path) {
|
||||
open_file(path);
|
||||
};
|
||||
}
|
||||
|
||||
EditorWrapper::~EditorWrapper()
|
||||
{
|
||||
}
|
||||
|
||||
void EditorWrapper::set_editor_has_focus(Badge<Editor>, bool focus)
|
||||
{
|
||||
m_filename_label->set_font(focus ? Gfx::FontDatabase::default_bold_font() : Gfx::FontDatabase::default_font());
|
||||
}
|
||||
|
||||
}
|
60
Userland/DevTools/HackStudio/EditorWrapper.h
Normal file
60
Userland/DevTools/HackStudio/EditorWrapper.h
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Debugger/BreakpointCallback.h"
|
||||
#include <AK/Function.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGUI/Widget.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class Editor;
|
||||
|
||||
class EditorWrapper : public GUI::Widget {
|
||||
C_OBJECT(EditorWrapper)
|
||||
public:
|
||||
virtual ~EditorWrapper() override;
|
||||
|
||||
Editor& editor() { return *m_editor; }
|
||||
const Editor& editor() const { return *m_editor; }
|
||||
|
||||
GUI::Label& filename_label() { return *m_filename_label; }
|
||||
const GUI::Label& filename_label() const { return *m_filename_label; }
|
||||
|
||||
void set_editor_has_focus(Badge<Editor>, bool);
|
||||
|
||||
private:
|
||||
EditorWrapper();
|
||||
|
||||
RefPtr<GUI::Label> m_filename_label;
|
||||
RefPtr<GUI::Label> m_cursor_label;
|
||||
RefPtr<Editor> m_editor;
|
||||
};
|
||||
|
||||
}
|
173
Userland/DevTools/HackStudio/FindInFilesWidget.cpp
Normal file
173
Userland/DevTools/HackStudio/FindInFilesWidget.cpp
Normal file
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "FindInFilesWidget.h"
|
||||
#include "HackStudio.h"
|
||||
#include "Project.h"
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibGUI/BoxLayout.h>
|
||||
#include <LibGUI/Button.h>
|
||||
#include <LibGUI/TableView.h>
|
||||
#include <LibGUI/TextBox.h>
|
||||
#include <LibGfx/FontDatabase.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
struct Match {
|
||||
String filename;
|
||||
GUI::TextRange range;
|
||||
String text;
|
||||
};
|
||||
|
||||
class SearchResultsModel final : public GUI::Model {
|
||||
public:
|
||||
enum Column {
|
||||
Filename,
|
||||
Location,
|
||||
MatchedText,
|
||||
__Count
|
||||
};
|
||||
|
||||
explicit SearchResultsModel(const Vector<Match>&& matches)
|
||||
: m_matches(move(matches))
|
||||
{
|
||||
}
|
||||
|
||||
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_matches.size(); }
|
||||
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; }
|
||||
|
||||
virtual String column_name(int column) const override
|
||||
{
|
||||
switch (column) {
|
||||
case Column::Filename:
|
||||
return "Filename";
|
||||
case Column::Location:
|
||||
return "#";
|
||||
case Column::MatchedText:
|
||||
return "Text";
|
||||
default:
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override
|
||||
{
|
||||
if (role == GUI::ModelRole::TextAlignment)
|
||||
return Gfx::TextAlignment::CenterLeft;
|
||||
if (role == GUI::ModelRole::Font) {
|
||||
if (index.column() == Column::MatchedText)
|
||||
return Gfx::FontDatabase::default_fixed_width_font();
|
||||
return {};
|
||||
}
|
||||
if (role == GUI::ModelRole::Display) {
|
||||
auto& match = m_matches.at(index.row());
|
||||
switch (index.column()) {
|
||||
case Column::Filename:
|
||||
return match.filename;
|
||||
case Column::Location:
|
||||
return (int)match.range.start().line();
|
||||
case Column::MatchedText:
|
||||
return match.text;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
virtual void update() override { }
|
||||
virtual GUI::ModelIndex index(int row, int column = 0, const GUI::ModelIndex& = GUI::ModelIndex()) const override
|
||||
{
|
||||
if (row < 0 || row >= (int)m_matches.size())
|
||||
return {};
|
||||
if (column < 0 || column >= Column::__Count)
|
||||
return {};
|
||||
return create_index(row, column, &m_matches.at(row));
|
||||
}
|
||||
|
||||
private:
|
||||
Vector<Match> m_matches;
|
||||
};
|
||||
|
||||
static RefPtr<SearchResultsModel> find_in_files(const StringView& text)
|
||||
{
|
||||
Vector<Match> matches;
|
||||
project().for_each_text_file([&](auto& file) {
|
||||
auto matches_in_file = file.document().find_all(text);
|
||||
for (auto& range : matches_in_file) {
|
||||
auto whole_line_range = file.document().range_for_entire_line(range.start().line());
|
||||
auto whole_line_containing_match = file.document().text_in_range(whole_line_range);
|
||||
auto left_part = whole_line_containing_match.substring(0, range.start().column());
|
||||
auto right_part = whole_line_containing_match.substring(range.end().column(), whole_line_containing_match.length() - range.end().column());
|
||||
StringBuilder builder;
|
||||
builder.append(left_part);
|
||||
builder.append(0x01);
|
||||
builder.append(file.document().text_in_range(range));
|
||||
builder.append(0x02);
|
||||
builder.append(right_part);
|
||||
matches.append({ file.name(), range, builder.to_string() });
|
||||
}
|
||||
});
|
||||
|
||||
return adopt(*new SearchResultsModel(move(matches)));
|
||||
}
|
||||
|
||||
FindInFilesWidget::FindInFilesWidget()
|
||||
{
|
||||
set_layout<GUI::VerticalBoxLayout>();
|
||||
|
||||
auto& top_container = add<Widget>();
|
||||
top_container.set_layout<GUI::HorizontalBoxLayout>();
|
||||
top_container.set_fixed_height(20);
|
||||
|
||||
m_textbox = top_container.add<GUI::TextBox>();
|
||||
|
||||
m_button = top_container.add<GUI::Button>("Find in files");
|
||||
m_button->set_fixed_width(100);
|
||||
|
||||
m_result_view = add<GUI::TableView>();
|
||||
|
||||
m_result_view->on_activation = [](auto& index) {
|
||||
auto& match = *(const Match*)index.internal_data();
|
||||
open_file(match.filename);
|
||||
current_editor().set_selection(match.range);
|
||||
current_editor().set_focus(true);
|
||||
};
|
||||
|
||||
m_button->on_click = [this](auto) {
|
||||
auto results_model = find_in_files(m_textbox->text());
|
||||
m_result_view->set_model(results_model);
|
||||
};
|
||||
m_textbox->on_return_pressed = [this] {
|
||||
m_button->click();
|
||||
};
|
||||
}
|
||||
|
||||
void FindInFilesWidget::focus_textbox_and_select_all()
|
||||
{
|
||||
m_textbox->select_all();
|
||||
m_textbox->set_focus(true);
|
||||
}
|
||||
|
||||
}
|
50
Userland/DevTools/HackStudio/FindInFilesWidget.h
Normal file
50
Userland/DevTools/HackStudio/FindInFilesWidget.h
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibGUI/Button.h>
|
||||
#include <LibGUI/TextBox.h>
|
||||
#include <LibGUI/Widget.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class FindInFilesWidget final : public GUI::Widget {
|
||||
C_OBJECT(FindInFilesWidget)
|
||||
public:
|
||||
virtual ~FindInFilesWidget() override { }
|
||||
|
||||
void focus_textbox_and_select_all();
|
||||
|
||||
private:
|
||||
explicit FindInFilesWidget();
|
||||
|
||||
RefPtr<GUI::TextBox> m_textbox;
|
||||
RefPtr<GUI::Button> m_button;
|
||||
RefPtr<GUI::TableView> m_result_view;
|
||||
};
|
||||
|
||||
}
|
68
Userland/DevTools/HackStudio/FormEditorWidget.cpp
Normal file
68
Userland/DevTools/HackStudio/FormEditorWidget.cpp
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "FormEditorWidget.h"
|
||||
#include "CursorTool.h"
|
||||
#include "FormWidget.h"
|
||||
#include "WidgetTreeModel.h"
|
||||
#include <LibGUI/Painter.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
FormEditorWidget::FormEditorWidget()
|
||||
: m_tool(make<CursorTool>(*this))
|
||||
{
|
||||
set_fill_with_background_color(true);
|
||||
|
||||
m_form_widget = add<FormWidget>();
|
||||
m_widget_tree_model = WidgetTreeModel::create(*m_form_widget);
|
||||
}
|
||||
|
||||
FormEditorWidget::~FormEditorWidget()
|
||||
{
|
||||
}
|
||||
|
||||
void FormEditorWidget::paint_event(GUI::PaintEvent& event)
|
||||
{
|
||||
GUI::Frame::paint_event(event);
|
||||
|
||||
GUI::Painter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
}
|
||||
|
||||
void FormEditorWidget::set_tool(NonnullOwnPtr<Tool> tool)
|
||||
{
|
||||
m_tool->detach();
|
||||
m_tool = move(tool);
|
||||
m_tool->attach();
|
||||
}
|
||||
|
||||
WidgetTreeModel& FormEditorWidget::model()
|
||||
{
|
||||
return *m_widget_tree_model;
|
||||
}
|
||||
|
||||
}
|
137
Userland/DevTools/HackStudio/FormEditorWidget.h
Normal file
137
Userland/DevTools/HackStudio/FormEditorWidget.h
Normal file
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/HashTable.h>
|
||||
#include <LibGUI/ScrollableWidget.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class FormWidget;
|
||||
class Tool;
|
||||
class WidgetTreeModel;
|
||||
|
||||
class FormEditorWidget final : public GUI::ScrollableWidget {
|
||||
C_OBJECT(FormEditorWidget)
|
||||
public:
|
||||
virtual ~FormEditorWidget() override;
|
||||
|
||||
FormWidget& form_widget() { return *m_form_widget; }
|
||||
const FormWidget& form_widget() const { return *m_form_widget; }
|
||||
|
||||
Tool& tool() { return *m_tool; }
|
||||
const Tool& tool() const { return *m_tool; }
|
||||
|
||||
void set_tool(NonnullOwnPtr<Tool>);
|
||||
|
||||
WidgetTreeModel& model();
|
||||
|
||||
class WidgetSelection {
|
||||
public:
|
||||
Function<void(GUI::Widget&)> on_remove;
|
||||
Function<void(GUI::Widget&)> on_add;
|
||||
Function<void()> on_clear;
|
||||
|
||||
void enable_hooks() { m_hooks_enabled = true; }
|
||||
void disable_hooks() { m_hooks_enabled = false; }
|
||||
|
||||
bool is_empty() const
|
||||
{
|
||||
return m_widgets.is_empty();
|
||||
}
|
||||
|
||||
bool contains(GUI::Widget& widget) const
|
||||
{
|
||||
return m_widgets.contains(&widget);
|
||||
}
|
||||
|
||||
void toggle(GUI::Widget& widget)
|
||||
{
|
||||
if (contains(widget))
|
||||
remove(widget);
|
||||
else
|
||||
add(widget);
|
||||
}
|
||||
|
||||
void set(GUI::Widget& widget)
|
||||
{
|
||||
clear();
|
||||
add(widget);
|
||||
}
|
||||
|
||||
void remove(GUI::Widget& widget)
|
||||
{
|
||||
ASSERT(m_widgets.contains(&widget));
|
||||
m_widgets.remove(&widget);
|
||||
if (m_hooks_enabled && on_remove)
|
||||
on_remove(widget);
|
||||
}
|
||||
|
||||
void add(GUI::Widget& widget)
|
||||
{
|
||||
m_widgets.set(&widget);
|
||||
if (m_hooks_enabled && on_add)
|
||||
on_add(widget);
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
m_widgets.clear();
|
||||
if (m_hooks_enabled && on_clear)
|
||||
on_clear();
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
void for_each(Callback callback)
|
||||
{
|
||||
for (auto& it : m_widgets) {
|
||||
if (callback(*it) == IterationDecision::Break)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
WidgetSelection() { }
|
||||
|
||||
private:
|
||||
HashTable<GUI::Widget*> m_widgets;
|
||||
bool m_hooks_enabled { true };
|
||||
};
|
||||
|
||||
WidgetSelection& selection() { return m_selection; }
|
||||
|
||||
private:
|
||||
virtual void paint_event(GUI::PaintEvent&) override;
|
||||
|
||||
FormEditorWidget();
|
||||
|
||||
RefPtr<FormWidget> m_form_widget;
|
||||
RefPtr<WidgetTreeModel> m_widget_tree_model;
|
||||
NonnullOwnPtr<Tool> m_tool;
|
||||
WidgetSelection m_selection;
|
||||
};
|
||||
|
||||
}
|
106
Userland/DevTools/HackStudio/FormWidget.cpp
Normal file
106
Userland/DevTools/HackStudio/FormWidget.cpp
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "FormWidget.h"
|
||||
#include "FormEditorWidget.h"
|
||||
#include "Tool.h"
|
||||
#include <LibGUI/Painter.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
FormWidget::FormWidget()
|
||||
{
|
||||
set_focus_policy(GUI::FocusPolicy::StrongFocus);
|
||||
set_fill_with_background_color(true);
|
||||
set_relative_rect(5, 5, 400, 300);
|
||||
|
||||
set_greedy_for_hits(true);
|
||||
}
|
||||
|
||||
FormWidget::~FormWidget()
|
||||
{
|
||||
}
|
||||
|
||||
FormEditorWidget& FormWidget::editor()
|
||||
{
|
||||
return static_cast<FormEditorWidget&>(*parent());
|
||||
}
|
||||
|
||||
const FormEditorWidget& FormWidget::editor() const
|
||||
{
|
||||
return static_cast<const FormEditorWidget&>(*parent());
|
||||
}
|
||||
|
||||
void FormWidget::paint_event(GUI::PaintEvent& event)
|
||||
{
|
||||
GUI::Painter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
|
||||
for (int y = 0; y < height(); y += m_grid_size) {
|
||||
for (int x = 0; x < width(); x += m_grid_size) {
|
||||
painter.set_pixel({ x, y }, Color::from_rgb(0x404040));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FormWidget::second_paint_event(GUI::PaintEvent& event)
|
||||
{
|
||||
GUI::Painter painter(*this);
|
||||
painter.add_clip_rect(event.rect());
|
||||
|
||||
if (!editor().selection().is_empty()) {
|
||||
for_each_child_widget([&](auto& child) {
|
||||
if (editor().selection().contains(child)) {
|
||||
painter.draw_rect(child.relative_rect(), Color::Blue);
|
||||
}
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
}
|
||||
|
||||
editor().tool().on_second_paint(painter, event);
|
||||
}
|
||||
|
||||
void FormWidget::mousedown_event(GUI::MouseEvent& event)
|
||||
{
|
||||
editor().tool().on_mousedown(event);
|
||||
}
|
||||
|
||||
void FormWidget::mouseup_event(GUI::MouseEvent& event)
|
||||
{
|
||||
editor().tool().on_mouseup(event);
|
||||
}
|
||||
|
||||
void FormWidget::mousemove_event(GUI::MouseEvent& event)
|
||||
{
|
||||
editor().tool().on_mousemove(event);
|
||||
}
|
||||
|
||||
void FormWidget::keydown_event(GUI::KeyEvent& event)
|
||||
{
|
||||
editor().tool().on_keydown(event);
|
||||
}
|
||||
|
||||
}
|
61
Userland/DevTools/HackStudio/FormWidget.h
Normal file
61
Userland/DevTools/HackStudio/FormWidget.h
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Badge.h>
|
||||
#include <LibGUI/Widget.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class CursorTool;
|
||||
class FormEditorWidget;
|
||||
|
||||
class FormWidget final : public GUI::Widget {
|
||||
C_OBJECT(FormWidget)
|
||||
public:
|
||||
virtual ~FormWidget() override;
|
||||
|
||||
FormEditorWidget& editor();
|
||||
const FormEditorWidget& editor() const;
|
||||
|
||||
// FIXME: This should be an app-wide preference instead.
|
||||
int grid_size() const { return m_grid_size; }
|
||||
|
||||
private:
|
||||
virtual void paint_event(GUI::PaintEvent&) override;
|
||||
virtual void second_paint_event(GUI::PaintEvent&) override;
|
||||
virtual void mousedown_event(GUI::MouseEvent&) override;
|
||||
virtual void mouseup_event(GUI::MouseEvent&) override;
|
||||
virtual void mousemove_event(GUI::MouseEvent&) override;
|
||||
virtual void keydown_event(GUI::KeyEvent&) override;
|
||||
|
||||
FormWidget();
|
||||
|
||||
int m_grid_size { 5 };
|
||||
};
|
||||
|
||||
}
|
253
Userland/DevTools/HackStudio/Git/DiffViewer.cpp
Normal file
253
Userland/DevTools/HackStudio/Git/DiffViewer.cpp
Normal file
|
@ -0,0 +1,253 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "DiffViewer.h"
|
||||
#include <LibDiff/Hunks.h>
|
||||
#include <LibGUI/AbstractView.h>
|
||||
#include <LibGUI/Painter.h>
|
||||
#include <LibGUI/ScrollBar.h>
|
||||
#include <LibGfx/Color.h>
|
||||
#include <LibGfx/Font.h>
|
||||
#include <LibGfx/FontDatabase.h>
|
||||
#include <LibGfx/Palette.h>
|
||||
|
||||
// #define DEBUG_DIFF
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
DiffViewer::~DiffViewer()
|
||||
{
|
||||
}
|
||||
|
||||
void DiffViewer::paint_event(GUI::PaintEvent& event)
|
||||
{
|
||||
GUI::Painter painter(*this);
|
||||
painter.add_clip_rect(widget_inner_rect());
|
||||
painter.add_clip_rect(event.rect());
|
||||
painter.fill_rect(event.rect(), palette().color(background_role()));
|
||||
painter.translate(frame_thickness(), frame_thickness());
|
||||
painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
|
||||
|
||||
// Why we need to translate here again? We've already translated the painter.
|
||||
// Anyways, it paints correctly so I'll leave it like this.
|
||||
painter.fill_rect_with_dither_pattern(
|
||||
separator_rect().translated(horizontal_scrollbar().value(), vertical_scrollbar().value()),
|
||||
Gfx::Color::LightGray,
|
||||
Gfx::Color::White);
|
||||
|
||||
size_t y_offset = 10;
|
||||
size_t current_original_line_index = 0;
|
||||
for (const auto& hunk : m_hunks) {
|
||||
for (size_t i = current_original_line_index; i < hunk.original_start_line; ++i) {
|
||||
draw_line(painter, m_original_lines[i], y_offset, LinePosition::Both, LineType::Normal);
|
||||
y_offset += line_height();
|
||||
}
|
||||
current_original_line_index = hunk.original_start_line + hunk.removed_lines.size();
|
||||
|
||||
size_t left_y_offset = y_offset;
|
||||
for (const auto& removed_line : hunk.removed_lines) {
|
||||
draw_line(painter, removed_line, left_y_offset, LinePosition::Left, LineType::Diff);
|
||||
left_y_offset += line_height();
|
||||
}
|
||||
for (int i = 0; i < (int)hunk.added_lines.size() - (int)hunk.removed_lines.size(); ++i) {
|
||||
draw_line(painter, "", left_y_offset, LinePosition::Left, LineType::Missing);
|
||||
left_y_offset += line_height();
|
||||
}
|
||||
|
||||
size_t right_y_offset = y_offset;
|
||||
for (const auto& added_line : hunk.added_lines) {
|
||||
draw_line(painter, added_line, right_y_offset, LinePosition::Right, LineType::Diff);
|
||||
right_y_offset += line_height();
|
||||
}
|
||||
for (int i = 0; i < (int)hunk.removed_lines.size() - (int)hunk.added_lines.size(); ++i) {
|
||||
draw_line(painter, "", right_y_offset, LinePosition::Right, LineType::Missing);
|
||||
right_y_offset += line_height();
|
||||
}
|
||||
|
||||
ASSERT(left_y_offset == right_y_offset);
|
||||
y_offset = left_y_offset;
|
||||
}
|
||||
for (size_t i = current_original_line_index; i < m_original_lines.size(); ++i) {
|
||||
draw_line(painter, m_original_lines[i], y_offset, LinePosition::Both, LineType::Normal);
|
||||
y_offset += line_height();
|
||||
}
|
||||
}
|
||||
|
||||
void DiffViewer::draw_line(GUI::Painter& painter, const String& line, size_t y_offset, LinePosition line_position, LineType line_type)
|
||||
{
|
||||
size_t line_width = font().width(line);
|
||||
|
||||
constexpr size_t padding = 10;
|
||||
size_t left_side_x_offset = padding;
|
||||
size_t right_side_x_offset = separator_rect().x() + padding;
|
||||
|
||||
// FIXME: Long lines will overflow out of their side of the diff view
|
||||
Gfx::IntRect left_line_rect { (int)left_side_x_offset, (int)y_offset, (int)line_width, (int)line_height() };
|
||||
Gfx::IntRect right_line_rect { (int)right_side_x_offset, (int)y_offset, (int)line_width, (int)line_height() };
|
||||
auto color = palette().color(foreground_role());
|
||||
|
||||
if (line_position == LinePosition::Left || line_position == LinePosition::Both) {
|
||||
painter.draw_text(left_line_rect, line, Gfx::TextAlignment::TopLeft, color);
|
||||
if (line_type != LineType::Normal) {
|
||||
Gfx::IntRect outline = { (int)left_side_x_offset, ((int)y_offset) - 2, separator_rect().x() - (int)(padding * 2), (int)line_height() };
|
||||
if (line_type == LineType::Diff) {
|
||||
painter.fill_rect(
|
||||
outline,
|
||||
red_background());
|
||||
}
|
||||
if (line_type == LineType::Missing) {
|
||||
painter.fill_rect(
|
||||
outline,
|
||||
gray_background());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (line_position == LinePosition::Right || line_position == LinePosition::Both) {
|
||||
painter.draw_text(right_line_rect, line, Gfx::TextAlignment::TopLeft, color);
|
||||
if (line_type != LineType::Normal) {
|
||||
Gfx::IntRect outline = { (int)right_side_x_offset, ((int)y_offset) - 2, frame_inner_rect().width() - separator_rect().x() - (int)(padding * 2) - 10, (int)line_height() };
|
||||
if (line_type == LineType::Diff) {
|
||||
painter.fill_rect(
|
||||
outline,
|
||||
green_background());
|
||||
}
|
||||
if (line_type == LineType::Missing) {
|
||||
painter.fill_rect(
|
||||
outline,
|
||||
gray_background());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t DiffViewer::line_height() const
|
||||
{
|
||||
return font().glyph_height() + 4;
|
||||
}
|
||||
|
||||
Gfx::IntRect DiffViewer::separator_rect() const
|
||||
{
|
||||
return Gfx::IntRect { frame_inner_rect().width() / 2 - 2,
|
||||
0,
|
||||
4,
|
||||
frame_inner_rect().height() };
|
||||
}
|
||||
|
||||
void DiffViewer::set_content(const String& original, const String& diff)
|
||||
{
|
||||
m_original_lines = split_to_lines(original);
|
||||
m_hunks = Diff::parse_hunks(diff);
|
||||
|
||||
#ifdef DEBUG_DIFF
|
||||
for (size_t i = 0; i < m_original_lines.size(); ++i)
|
||||
dbgln("{}:{}", i, m_original_lines[i]);
|
||||
#endif
|
||||
}
|
||||
|
||||
DiffViewer::DiffViewer()
|
||||
{
|
||||
setup_properties();
|
||||
}
|
||||
|
||||
DiffViewer::DiffViewer(const String& original, const String& diff)
|
||||
: m_original_lines(split_to_lines(original))
|
||||
, m_hunks(Diff::parse_hunks(diff))
|
||||
{
|
||||
setup_properties();
|
||||
}
|
||||
|
||||
void DiffViewer::setup_properties()
|
||||
{
|
||||
set_font(Gfx::FontDatabase::default_fixed_width_font());
|
||||
set_background_role(ColorRole::Base);
|
||||
set_foreground_role(ColorRole::BaseText);
|
||||
}
|
||||
|
||||
Vector<String> DiffViewer::split_to_lines(const String& text)
|
||||
{
|
||||
// NOTE: This is slightly different than text.split('\n')
|
||||
Vector<String> lines;
|
||||
size_t next_line_start_index = 0;
|
||||
for (size_t i = 0; i < text.length(); ++i) {
|
||||
if (text[i] == '\n') {
|
||||
auto line_text = text.substring(next_line_start_index, i - next_line_start_index);
|
||||
lines.append(move(line_text));
|
||||
next_line_start_index = i + 1;
|
||||
}
|
||||
}
|
||||
lines.append(text.substring(next_line_start_index, text.length() - next_line_start_index));
|
||||
return lines;
|
||||
}
|
||||
|
||||
Gfx::Color DiffViewer::red_background()
|
||||
{
|
||||
static Gfx::Color color = Gfx::Color::from_rgba(0x88ff0000);
|
||||
return color;
|
||||
}
|
||||
|
||||
Gfx::Color DiffViewer::green_background()
|
||||
{
|
||||
static Gfx::Color color = Gfx::Color::from_rgba(0x8800ff00);
|
||||
return color;
|
||||
}
|
||||
|
||||
Gfx::Color DiffViewer::gray_background()
|
||||
{
|
||||
static Gfx::Color color = Gfx::Color::from_rgba(0x88888888);
|
||||
return color;
|
||||
}
|
||||
|
||||
void DiffViewer::update_content_size()
|
||||
{
|
||||
if (m_hunks.is_empty()) {
|
||||
set_content_size({ 0, 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
size_t num_lines = 0;
|
||||
size_t current_original_line_index = 0;
|
||||
for (const auto& hunk : m_hunks) {
|
||||
num_lines += ((int)hunk.original_start_line - (int)current_original_line_index);
|
||||
|
||||
num_lines += hunk.removed_lines.size();
|
||||
if (hunk.added_lines.size() > hunk.removed_lines.size()) {
|
||||
num_lines += ((int)hunk.added_lines.size() - (int)hunk.removed_lines.size());
|
||||
}
|
||||
current_original_line_index = hunk.original_start_line + hunk.removed_lines.size();
|
||||
}
|
||||
num_lines += ((int)m_original_lines.size() - (int)current_original_line_index);
|
||||
|
||||
// TODO: Support Horizontal scrolling
|
||||
set_content_size({ 0, (int)(num_lines * line_height()) });
|
||||
}
|
||||
|
||||
void DiffViewer::resize_event(GUI::ResizeEvent& event)
|
||||
{
|
||||
ScrollableWidget::resize_event(event);
|
||||
update_content_size();
|
||||
}
|
||||
|
||||
}
|
80
Userland/DevTools/HackStudio/Git/DiffViewer.h
Normal file
80
Userland/DevTools/HackStudio/Git/DiffViewer.h
Normal file
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/String.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibDiff/Hunks.h>
|
||||
#include <LibGUI/ScrollableWidget.h>
|
||||
|
||||
namespace HackStudio {
|
||||
class DiffViewer final : public GUI::ScrollableWidget {
|
||||
C_OBJECT(DiffViewer)
|
||||
public:
|
||||
virtual ~DiffViewer() override;
|
||||
|
||||
void set_content(const String& original, const String& diff);
|
||||
|
||||
private:
|
||||
DiffViewer(const String& original, const String& diff);
|
||||
DiffViewer();
|
||||
|
||||
void setup_properties();
|
||||
|
||||
virtual void paint_event(GUI::PaintEvent&) override;
|
||||
virtual void resize_event(GUI::ResizeEvent&) override;
|
||||
|
||||
void update_content_size();
|
||||
|
||||
enum class LinePosition {
|
||||
Left,
|
||||
Right,
|
||||
Both,
|
||||
};
|
||||
|
||||
enum class LineType {
|
||||
Normal,
|
||||
Diff,
|
||||
Missing,
|
||||
};
|
||||
|
||||
void draw_line(GUI::Painter&, const String& line, size_t y_offset, LinePosition, LineType);
|
||||
|
||||
static Vector<String> split_to_lines(const String& text);
|
||||
|
||||
static Gfx::Color red_background();
|
||||
static Gfx::Color green_background();
|
||||
static Gfx::Color gray_background();
|
||||
|
||||
size_t line_height() const;
|
||||
|
||||
Gfx::IntRect separator_rect() const;
|
||||
|
||||
Vector<String> m_original_lines;
|
||||
Vector<Diff::Hunk> m_hunks;
|
||||
};
|
||||
}
|
56
Userland/DevTools/HackStudio/Git/GitFilesModel.cpp
Normal file
56
Userland/DevTools/HackStudio/Git/GitFilesModel.cpp
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "GitFilesModel.h"
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
NonnullRefPtr<GitFilesModel> GitFilesModel::create(Vector<LexicalPath>&& files)
|
||||
{
|
||||
return adopt(*new GitFilesModel(move(files)));
|
||||
}
|
||||
|
||||
GitFilesModel::GitFilesModel(Vector<LexicalPath>&& files)
|
||||
: m_files(move(files))
|
||||
{
|
||||
}
|
||||
|
||||
GUI::Variant GitFilesModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
|
||||
{
|
||||
if (role == GUI::ModelRole::Display) {
|
||||
return m_files.at(index.row()).string();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
GUI::ModelIndex GitFilesModel::index(int row, int column, const GUI::ModelIndex&) const
|
||||
{
|
||||
if (row < 0 || row >= static_cast<int>(m_files.size()))
|
||||
return {};
|
||||
return create_index(row, column, &m_files.at(row));
|
||||
}
|
||||
|
||||
};
|
57
Userland/DevTools/HackStudio/Git/GitFilesModel.h
Normal file
57
Userland/DevTools/HackStudio/Git/GitFilesModel.h
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GitRepo.h"
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/NonnullRefPtr.h>
|
||||
#include <LibGUI/Model.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class GitFilesModel final : public GUI::Model {
|
||||
public:
|
||||
static NonnullRefPtr<GitFilesModel> create(Vector<LexicalPath>&& files);
|
||||
|
||||
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_files.size(); }
|
||||
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return 1; }
|
||||
|
||||
virtual String column_name(int) const override
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
|
||||
|
||||
virtual void update() override { }
|
||||
virtual GUI::ModelIndex index(int row, int column, const GUI::ModelIndex&) const override;
|
||||
|
||||
private:
|
||||
explicit GitFilesModel(Vector<LexicalPath>&& files);
|
||||
Vector<LexicalPath> m_files;
|
||||
};
|
||||
}
|
82
Userland/DevTools/HackStudio/Git/GitFilesView.cpp
Normal file
82
Userland/DevTools/HackStudio/Git/GitFilesView.cpp
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "GitFilesView.h"
|
||||
#include <LibGUI/Model.h>
|
||||
#include <LibGUI/Painter.h>
|
||||
#include <LibGUI/ScrollBar.h>
|
||||
#include <LibGfx/Palette.h>
|
||||
|
||||
namespace HackStudio {
|
||||
GitFilesView::~GitFilesView()
|
||||
{
|
||||
}
|
||||
|
||||
void GitFilesView::paint_list_item(GUI::Painter& painter, int row_index, int painted_item_index)
|
||||
{
|
||||
ListView::paint_list_item(painter, row_index, painted_item_index);
|
||||
|
||||
painter.blit(action_icon_rect((size_t)painted_item_index).top_left(), *m_action_icon, m_action_icon->rect());
|
||||
}
|
||||
|
||||
Gfx::IntRect GitFilesView::action_icon_rect(size_t painted_item_index)
|
||||
{
|
||||
int y = painted_item_index * item_height();
|
||||
return { content_width() - 20, y, m_action_icon->rect().width(), m_action_icon->rect().height() };
|
||||
}
|
||||
|
||||
GitFilesView::GitFilesView(GitFileActionCallback callback, NonnullRefPtr<Gfx::Bitmap> action_icon)
|
||||
: m_action_callback(move(callback))
|
||||
, m_action_icon(action_icon)
|
||||
{
|
||||
set_alternating_row_colors(false);
|
||||
}
|
||||
|
||||
void GitFilesView::mousedown_event(GUI::MouseEvent& event)
|
||||
{
|
||||
if (event.button() != GUI::MouseButton::Left) {
|
||||
ListView::mousedown_event(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.x() < action_icon_rect(0).x() || event.x() > action_icon_rect(0).top_right().x()) {
|
||||
ListView::mousedown_event(event);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t item_index = (event.y() + vertical_scrollbar().value()) / item_height();
|
||||
if (model()->row_count() == 0 || item_index > (size_t)model()->row_count()) {
|
||||
ListView::mousedown_event(event);
|
||||
return;
|
||||
}
|
||||
|
||||
auto data = model()->index(item_index, model_column()).data();
|
||||
|
||||
ASSERT(data.is_string());
|
||||
m_action_callback(LexicalPath(data.to_string()));
|
||||
}
|
||||
|
||||
};
|
56
Userland/DevTools/HackStudio/Git/GitFilesView.h
Normal file
56
Userland/DevTools/HackStudio/Git/GitFilesView.h
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <LibGUI/ListView.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
// A "GitFileAction" is either the staging or the unstaging of a file.
|
||||
typedef Function<void(const LexicalPath& file)> GitFileActionCallback;
|
||||
|
||||
class GitFilesView : public GUI::ListView {
|
||||
C_OBJECT(GitFilesView)
|
||||
public:
|
||||
virtual ~GitFilesView() override;
|
||||
|
||||
protected:
|
||||
GitFilesView(GitFileActionCallback, NonnullRefPtr<Gfx::Bitmap> action_icon);
|
||||
|
||||
private:
|
||||
virtual void paint_list_item(GUI::Painter& painter, int row_index, int painted_item_index);
|
||||
|
||||
virtual void mousedown_event(GUI::MouseEvent&) override;
|
||||
virtual Gfx::IntRect action_icon_rect(size_t painted_item_index);
|
||||
|
||||
GitFileActionCallback m_action_callback;
|
||||
NonnullRefPtr<Gfx::Bitmap> m_action_icon;
|
||||
};
|
||||
|
||||
}
|
151
Userland/DevTools/HackStudio/Git/GitRepo.cpp
Normal file
151
Userland/DevTools/HackStudio/Git/GitRepo.cpp
Normal file
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "GitRepo.h"
|
||||
#include <LibCore/Command.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
GitRepo::CreateResult GitRepo::try_to_create(const LexicalPath& repository_root)
|
||||
{
|
||||
if (!git_is_installed()) {
|
||||
return { CreateResult::Type::GitProgramNotFound, nullptr };
|
||||
}
|
||||
if (!git_repo_exists(repository_root)) {
|
||||
return { CreateResult::Type::NoGitRepo, nullptr };
|
||||
}
|
||||
|
||||
return { CreateResult::Type::Success, adopt(*new GitRepo(repository_root)) };
|
||||
}
|
||||
|
||||
RefPtr<GitRepo> GitRepo::initialize_repository(const LexicalPath& repository_root)
|
||||
{
|
||||
auto res = command_wrapper({ "init" }, repository_root);
|
||||
if (res.is_null())
|
||||
return {};
|
||||
|
||||
ASSERT(git_repo_exists(repository_root));
|
||||
return adopt(*new GitRepo(repository_root));
|
||||
}
|
||||
|
||||
Vector<LexicalPath> GitRepo::unstaged_files() const
|
||||
{
|
||||
auto modified = modified_files();
|
||||
auto untracked = untracked_files();
|
||||
modified.append(move(untracked));
|
||||
return modified;
|
||||
}
|
||||
//
|
||||
Vector<LexicalPath> GitRepo::staged_files() const
|
||||
{
|
||||
auto raw_result = command({ "diff", "--cached", "--name-only" });
|
||||
if (raw_result.is_null())
|
||||
return {};
|
||||
return parse_files_list(raw_result);
|
||||
}
|
||||
|
||||
Vector<LexicalPath> GitRepo::modified_files() const
|
||||
{
|
||||
auto raw_result = command({ "ls-files", "--modified", "--exclude-standard" });
|
||||
if (raw_result.is_null())
|
||||
return {};
|
||||
return parse_files_list(raw_result);
|
||||
}
|
||||
|
||||
Vector<LexicalPath> GitRepo::untracked_files() const
|
||||
{
|
||||
auto raw_result = command({ "ls-files", "--others", "--exclude-standard" });
|
||||
if (raw_result.is_null())
|
||||
return {};
|
||||
return parse_files_list(raw_result);
|
||||
}
|
||||
|
||||
Vector<LexicalPath> GitRepo::parse_files_list(const String& raw_result)
|
||||
{
|
||||
auto lines = raw_result.split('\n');
|
||||
Vector<LexicalPath> files;
|
||||
for (const auto& line : lines) {
|
||||
files.empend(line);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
String GitRepo::command(const Vector<String>& command_parts) const
|
||||
{
|
||||
return command_wrapper(command_parts, m_repository_root);
|
||||
}
|
||||
|
||||
String GitRepo::command_wrapper(const Vector<String>& command_parts, const LexicalPath& chdir)
|
||||
{
|
||||
return Core::command("git", command_parts, chdir);
|
||||
}
|
||||
|
||||
bool GitRepo::git_is_installed()
|
||||
{
|
||||
return !command_wrapper({ "--help" }, LexicalPath("/")).is_null();
|
||||
}
|
||||
|
||||
bool GitRepo::git_repo_exists(const LexicalPath& repo_root)
|
||||
{
|
||||
return !command_wrapper({ "status" }, repo_root).is_null();
|
||||
}
|
||||
|
||||
bool GitRepo::stage(const LexicalPath& file)
|
||||
{
|
||||
return !command({ "add", file.string() }).is_null();
|
||||
}
|
||||
|
||||
bool GitRepo::unstage(const LexicalPath& file)
|
||||
{
|
||||
return !command({ "reset", "HEAD", "--", file.string() }).is_null();
|
||||
}
|
||||
|
||||
bool GitRepo::commit(const String& message)
|
||||
{
|
||||
return !command({ "commit", "-m", message }).is_null();
|
||||
}
|
||||
|
||||
Optional<String> GitRepo::original_file_content(const LexicalPath& file) const
|
||||
{
|
||||
return command({ "show", String::formatted("HEAD:{}", file) });
|
||||
}
|
||||
|
||||
Optional<String> GitRepo::unstaged_diff(const LexicalPath& file) const
|
||||
{
|
||||
return command({ "diff", file.string().characters() });
|
||||
}
|
||||
|
||||
bool GitRepo::is_tracked(const LexicalPath& file) const
|
||||
{
|
||||
auto res = command({ "ls-files", file.string() });
|
||||
if (res.is_null())
|
||||
return false;
|
||||
return !res.is_empty();
|
||||
}
|
||||
|
||||
}
|
81
Userland/DevTools/HackStudio/Git/GitRepo.h
Normal file
81
Userland/DevTools/HackStudio/Git/GitRepo.h
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/Optional.h>
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/RefPtr.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/Vector.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class GitRepo final : public RefCounted<GitRepo> {
|
||||
public:
|
||||
struct CreateResult {
|
||||
enum class Type {
|
||||
Success,
|
||||
NoGitRepo,
|
||||
GitProgramNotFound,
|
||||
};
|
||||
Type type;
|
||||
RefPtr<GitRepo> repo;
|
||||
};
|
||||
|
||||
static CreateResult try_to_create(const LexicalPath& repository_root);
|
||||
static RefPtr<GitRepo> initialize_repository(const LexicalPath& repository_root);
|
||||
|
||||
Vector<LexicalPath> unstaged_files() const;
|
||||
Vector<LexicalPath> staged_files() const;
|
||||
bool stage(const LexicalPath& file);
|
||||
bool unstage(const LexicalPath& file);
|
||||
bool commit(const String& message);
|
||||
Optional<String> original_file_content(const LexicalPath& file) const;
|
||||
Optional<String> unstaged_diff(const LexicalPath& file) const;
|
||||
bool is_tracked(const LexicalPath& file) const;
|
||||
|
||||
private:
|
||||
static String command_wrapper(const Vector<String>& command_parts, const LexicalPath& chdir);
|
||||
static bool git_is_installed();
|
||||
static bool git_repo_exists(const LexicalPath& repo_root);
|
||||
static Vector<LexicalPath> parse_files_list(const String&);
|
||||
|
||||
explicit GitRepo(const LexicalPath& repository_root)
|
||||
: m_repository_root(repository_root)
|
||||
{
|
||||
}
|
||||
|
||||
Vector<LexicalPath> modified_files() const;
|
||||
Vector<LexicalPath> untracked_files() const;
|
||||
|
||||
String command(const Vector<String>& command_parts) const;
|
||||
|
||||
LexicalPath m_repository_root;
|
||||
};
|
||||
|
||||
}
|
188
Userland/DevTools/HackStudio/Git/GitWidget.cpp
Normal file
188
Userland/DevTools/HackStudio/Git/GitWidget.cpp
Normal file
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "GitWidget.h"
|
||||
#include "GitFilesModel.h"
|
||||
#include <AK/LogStream.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibDiff/Format.h>
|
||||
#include <LibGUI/Application.h>
|
||||
#include <LibGUI/BoxLayout.h>
|
||||
#include <LibGUI/Button.h>
|
||||
#include <LibGUI/InputBox.h>
|
||||
#include <LibGUI/Label.h>
|
||||
#include <LibGUI/MessageBox.h>
|
||||
#include <LibGUI/Model.h>
|
||||
#include <LibGUI/Painter.h>
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
GitWidget::GitWidget(const LexicalPath& repo_root)
|
||||
: m_repo_root(repo_root)
|
||||
{
|
||||
set_layout<GUI::HorizontalBoxLayout>();
|
||||
|
||||
auto& unstaged = add<GUI::Widget>();
|
||||
unstaged.set_layout<GUI::VerticalBoxLayout>();
|
||||
auto& unstaged_header = unstaged.add<GUI::Widget>();
|
||||
unstaged_header.set_layout<GUI::HorizontalBoxLayout>();
|
||||
|
||||
auto& refresh_button = unstaged_header.add<GUI::Button>();
|
||||
refresh_button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/reload.png"));
|
||||
refresh_button.set_fixed_size(16, 16);
|
||||
refresh_button.set_tooltip("refresh");
|
||||
refresh_button.on_click = [this](int) { refresh(); };
|
||||
|
||||
auto& unstaged_label = unstaged_header.add<GUI::Label>();
|
||||
unstaged_label.set_text("Unstaged");
|
||||
|
||||
unstaged_header.set_fixed_height(20);
|
||||
m_unstaged_files = unstaged.add<GitFilesView>(
|
||||
[this](const auto& file) { stage_file(file); },
|
||||
Gfx::Bitmap::load_from_file("/res/icons/16x16/plus.png").release_nonnull());
|
||||
m_unstaged_files->on_selection = [this](const GUI::ModelIndex& index) {
|
||||
const auto& selected = index.data().as_string();
|
||||
show_diff(LexicalPath(selected));
|
||||
};
|
||||
|
||||
auto& staged = add<GUI::Widget>();
|
||||
staged.set_layout<GUI::VerticalBoxLayout>();
|
||||
|
||||
auto& staged_header = staged.add<GUI::Widget>();
|
||||
staged_header.set_layout<GUI::HorizontalBoxLayout>();
|
||||
|
||||
auto& commit_button = staged_header.add<GUI::Button>();
|
||||
commit_button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/commit.png"));
|
||||
commit_button.set_fixed_size(16, 16);
|
||||
commit_button.set_tooltip("commit");
|
||||
commit_button.on_click = [this](int) { commit(); };
|
||||
|
||||
auto& staged_label = staged_header.add<GUI::Label>();
|
||||
staged_label.set_text("Staged");
|
||||
|
||||
staged_header.set_fixed_height(20);
|
||||
m_staged_files = staged.add<GitFilesView>(
|
||||
[this](const auto& file) { unstage_file(file); },
|
||||
Gfx::Bitmap::load_from_file("/res/icons/16x16/minus.png").release_nonnull());
|
||||
}
|
||||
|
||||
bool GitWidget::initialize()
|
||||
{
|
||||
auto result = GitRepo::try_to_create(m_repo_root);
|
||||
switch (result.type) {
|
||||
case GitRepo::CreateResult::Type::Success:
|
||||
m_git_repo = result.repo;
|
||||
return true;
|
||||
case GitRepo::CreateResult::Type::GitProgramNotFound:
|
||||
GUI::MessageBox::show(window(), "Please install the Git port", "Error", GUI::MessageBox::Type::Error);
|
||||
return false;
|
||||
case GitRepo::CreateResult::Type::NoGitRepo: {
|
||||
auto decision = GUI::MessageBox::show(window(), "Create git repository?", "Git", GUI::MessageBox::Type::Question, GUI::MessageBox::InputType::YesNo);
|
||||
if (decision != GUI::Dialog::ExecResult::ExecYes)
|
||||
return false;
|
||||
m_git_repo = GitRepo::initialize_repository(m_repo_root);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
bool GitWidget::initialize_if_needed()
|
||||
{
|
||||
if (initialized())
|
||||
return true;
|
||||
|
||||
return initialize();
|
||||
}
|
||||
|
||||
void GitWidget::refresh()
|
||||
{
|
||||
if (!initialize_if_needed()) {
|
||||
dbgln("GitWidget initialization failed");
|
||||
return;
|
||||
}
|
||||
|
||||
ASSERT(!m_git_repo.is_null());
|
||||
|
||||
m_unstaged_files->set_model(GitFilesModel::create(m_git_repo->unstaged_files()));
|
||||
m_staged_files->set_model(GitFilesModel::create(m_git_repo->staged_files()));
|
||||
}
|
||||
|
||||
void GitWidget::stage_file(const LexicalPath& file)
|
||||
{
|
||||
dbgln("staging: {}", file);
|
||||
bool rc = m_git_repo->stage(file);
|
||||
ASSERT(rc);
|
||||
refresh();
|
||||
}
|
||||
|
||||
void GitWidget::unstage_file(const LexicalPath& file)
|
||||
{
|
||||
dbgln("unstaging: {}", file);
|
||||
bool rc = m_git_repo->unstage(file);
|
||||
ASSERT(rc);
|
||||
refresh();
|
||||
}
|
||||
|
||||
void GitWidget::commit()
|
||||
{
|
||||
String message;
|
||||
auto res = GUI::InputBox::show(message, window(), "Commit message:", "Commit");
|
||||
if (res != GUI::InputBox::ExecOK || message.is_empty())
|
||||
return;
|
||||
dbgln("commit message: {}", message);
|
||||
m_git_repo->commit(message);
|
||||
refresh();
|
||||
}
|
||||
|
||||
void GitWidget::set_view_diff_callback(ViewDiffCallback callback)
|
||||
{
|
||||
m_view_diff_callback = move(callback);
|
||||
}
|
||||
|
||||
void GitWidget::show_diff(const LexicalPath& file_path)
|
||||
{
|
||||
if (!m_git_repo->is_tracked(file_path)) {
|
||||
auto file = Core::File::construct(file_path.string());
|
||||
if (!file->open(Core::IODevice::ReadOnly)) {
|
||||
perror("open");
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
auto content = file->read_all();
|
||||
String content_string((char*)content.data(), content.size());
|
||||
m_view_diff_callback("", Diff::generate_only_additions(content_string));
|
||||
return;
|
||||
}
|
||||
const auto& original_content = m_git_repo->original_file_content(file_path);
|
||||
const auto& diff = m_git_repo->unstaged_diff(file_path);
|
||||
ASSERT(original_content.has_value() && diff.has_value());
|
||||
m_view_diff_callback(original_content.value(), diff.value());
|
||||
}
|
||||
}
|
65
Userland/DevTools/HackStudio/Git/GitWidget.h
Normal file
65
Userland/DevTools/HackStudio/Git/GitWidget.h
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "GitFilesView.h"
|
||||
#include "GitRepo.h"
|
||||
#include <AK/Function.h>
|
||||
#include <LibGUI/Forward.h>
|
||||
#include <LibGUI/Widget.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
typedef Function<void(const String& original_content, const String& diff)> ViewDiffCallback;
|
||||
|
||||
class GitWidget final : public GUI::Widget {
|
||||
C_OBJECT(GitWidget)
|
||||
public:
|
||||
virtual ~GitWidget() override { }
|
||||
|
||||
void refresh();
|
||||
void set_view_diff_callback(ViewDiffCallback callback);
|
||||
bool initialized() const { return !m_git_repo.is_null(); };
|
||||
|
||||
private:
|
||||
explicit GitWidget(const LexicalPath& repo_root);
|
||||
|
||||
bool initialize();
|
||||
bool initialize_if_needed();
|
||||
void stage_file(const LexicalPath&);
|
||||
void unstage_file(const LexicalPath&);
|
||||
void commit();
|
||||
void show_diff(const LexicalPath&);
|
||||
|
||||
LexicalPath m_repo_root;
|
||||
RefPtr<GitFilesView> m_unstaged_files;
|
||||
RefPtr<GitFilesView> m_staged_files;
|
||||
RefPtr<GitRepo> m_git_repo;
|
||||
ViewDiffCallback m_view_diff_callback;
|
||||
};
|
||||
|
||||
}
|
45
Userland/DevTools/HackStudio/HackStudio.h
Normal file
45
Userland/DevTools/HackStudio/HackStudio.h
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "EditorWrapper.h"
|
||||
#include "LanguageClients/ServerConnections.h"
|
||||
#include "Project.h"
|
||||
#include <AK/String.h>
|
||||
#include <LibGUI/TextEditor.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
GUI::TextEditor& current_editor();
|
||||
void open_file(const String&);
|
||||
RefPtr<EditorWrapper> current_editor_wrapper();
|
||||
void open_file(const String&);
|
||||
Project& project();
|
||||
String currently_open_file();
|
||||
void set_current_editor_wrapper(RefPtr<EditorWrapper>);
|
||||
|
||||
}
|
945
Userland/DevTools/HackStudio/HackStudioWidget.cpp
Normal file
945
Userland/DevTools/HackStudio/HackStudioWidget.cpp
Normal file
|
@ -0,0 +1,945 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* Copyright (c) 2020, the SerenityOS developers
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "HackStudioWidget.h"
|
||||
#include "CursorTool.h"
|
||||
#include "Debugger/DebugInfoWidget.h"
|
||||
#include "Debugger/Debugger.h"
|
||||
#include "Debugger/DisassemblyWidget.h"
|
||||
#include "Editor.h"
|
||||
#include "EditorWrapper.h"
|
||||
#include "FindInFilesWidget.h"
|
||||
#include "FormEditorWidget.h"
|
||||
#include "FormWidget.h"
|
||||
#include "Git/DiffViewer.h"
|
||||
#include "Git/GitWidget.h"
|
||||
#include "HackStudio.h"
|
||||
#include "HackStudioWidget.h"
|
||||
#include "Locator.h"
|
||||
#include "Project.h"
|
||||
#include "TerminalWrapper.h"
|
||||
#include "WidgetTool.h"
|
||||
#include "WidgetTreeModel.h"
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibCore/ArgsParser.h>
|
||||
#include <LibCore/Event.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibDebug/DebugSession.h>
|
||||
#include <LibGUI/Action.h>
|
||||
#include <LibGUI/ActionGroup.h>
|
||||
#include <LibGUI/Application.h>
|
||||
#include <LibGUI/BoxLayout.h>
|
||||
#include <LibGUI/Button.h>
|
||||
#include <LibGUI/EditingEngine.h>
|
||||
#include <LibGUI/FilePicker.h>
|
||||
#include <LibGUI/InputBox.h>
|
||||
#include <LibGUI/ItemListModel.h>
|
||||
#include <LibGUI/Label.h>
|
||||
#include <LibGUI/Menu.h>
|
||||
#include <LibGUI/MenuBar.h>
|
||||
#include <LibGUI/MessageBox.h>
|
||||
#include <LibGUI/RegularEditingEngine.h>
|
||||
#include <LibGUI/Splitter.h>
|
||||
#include <LibGUI/StackWidget.h>
|
||||
#include <LibGUI/TabWidget.h>
|
||||
#include <LibGUI/TableView.h>
|
||||
#include <LibGUI/TextBox.h>
|
||||
#include <LibGUI/TextEditor.h>
|
||||
#include <LibGUI/ToolBar.h>
|
||||
#include <LibGUI/ToolBarContainer.h>
|
||||
#include <LibGUI/TreeView.h>
|
||||
#include <LibGUI/VimEditingEngine.h>
|
||||
#include <LibGUI/Widget.h>
|
||||
#include <LibGUI/Window.h>
|
||||
#include <LibGfx/FontDatabase.h>
|
||||
#include <LibThread/Lock.h>
|
||||
#include <LibThread/Thread.h>
|
||||
#include <LibVT/TerminalWidget.h>
|
||||
#include <fcntl.h>
|
||||
#include <spawn.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
HackStudioWidget::HackStudioWidget(const String& path_to_project)
|
||||
{
|
||||
set_fill_with_background_color(true);
|
||||
set_layout<GUI::VerticalBoxLayout>();
|
||||
layout()->set_spacing(2);
|
||||
|
||||
open_project(path_to_project);
|
||||
|
||||
auto& toolbar_container = add<GUI::ToolBarContainer>();
|
||||
|
||||
auto& outer_splitter = add<GUI::HorizontalSplitter>();
|
||||
|
||||
auto& left_hand_splitter = outer_splitter.add<GUI::VerticalSplitter>();
|
||||
left_hand_splitter.set_fixed_width(150);
|
||||
create_project_tree_view(left_hand_splitter);
|
||||
m_project_tree_view_context_menu = create_project_tree_view_context_menu();
|
||||
|
||||
create_open_files_view(left_hand_splitter);
|
||||
|
||||
m_right_hand_splitter = outer_splitter.add<GUI::VerticalSplitter>();
|
||||
m_right_hand_stack = m_right_hand_splitter->add<GUI::StackWidget>();
|
||||
|
||||
// Put a placeholder widget front & center since we don't have a file open yet.
|
||||
m_right_hand_stack->add<GUI::Widget>();
|
||||
|
||||
create_form_editor(*m_right_hand_stack);
|
||||
|
||||
m_diff_viewer = m_right_hand_stack->add<DiffViewer>();
|
||||
|
||||
m_editors_splitter = m_right_hand_stack->add<GUI::VerticalSplitter>();
|
||||
m_editors_splitter->layout()->set_margins({ 0, 3, 0, 0 });
|
||||
add_new_editor(*m_editors_splitter);
|
||||
|
||||
m_switch_to_next_editor = create_switch_to_next_editor_action();
|
||||
m_switch_to_previous_editor = create_switch_to_previous_editor_action();
|
||||
|
||||
m_remove_current_editor_action = create_remove_current_editor_action();
|
||||
m_open_action = create_open_action();
|
||||
m_save_action = create_save_action();
|
||||
|
||||
create_action_tab(*m_right_hand_splitter);
|
||||
|
||||
m_add_editor_action = create_add_editor_action();
|
||||
m_add_terminal_action = create_add_terminal_action();
|
||||
m_remove_current_terminal_action = create_remove_current_terminal_action();
|
||||
|
||||
m_locator = add<Locator>();
|
||||
|
||||
m_terminal_wrapper->on_command_exit = [this] {
|
||||
m_stop_action->set_enabled(false);
|
||||
};
|
||||
|
||||
m_build_action = create_build_action();
|
||||
m_run_action = create_run_action();
|
||||
m_stop_action = create_stop_action();
|
||||
m_debug_action = create_debug_action();
|
||||
|
||||
initialize_debugger();
|
||||
|
||||
create_toolbar(toolbar_container);
|
||||
}
|
||||
|
||||
void HackStudioWidget::update_actions()
|
||||
{
|
||||
auto is_remove_terminal_enabled = [this]() {
|
||||
auto widget = m_action_tab_widget->active_widget();
|
||||
if (!widget)
|
||||
return false;
|
||||
if (StringView { "TerminalWrapper" } != widget->class_name())
|
||||
return false;
|
||||
if (!reinterpret_cast<TerminalWrapper*>(widget)->user_spawned())
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
m_remove_current_editor_action->set_enabled(m_all_editor_wrappers.size() > 1);
|
||||
m_remove_current_terminal_action->set_enabled(is_remove_terminal_enabled());
|
||||
}
|
||||
|
||||
void HackStudioWidget::on_action_tab_change()
|
||||
{
|
||||
update_actions();
|
||||
auto git_widget = m_action_tab_widget->active_widget();
|
||||
if (!git_widget)
|
||||
return;
|
||||
if (StringView { "GitWidget" } != git_widget->class_name())
|
||||
return;
|
||||
reinterpret_cast<GitWidget*>(git_widget)->refresh();
|
||||
}
|
||||
|
||||
void HackStudioWidget::open_project(const String& root_path)
|
||||
{
|
||||
if (chdir(root_path.characters()) < 0) {
|
||||
perror("chdir");
|
||||
exit(1);
|
||||
}
|
||||
m_project = Project::open_with_root_path(root_path);
|
||||
ASSERT(m_project);
|
||||
if (m_project_tree_view) {
|
||||
m_project_tree_view->set_model(m_project->model());
|
||||
m_project_tree_view->toggle_index(m_project_tree_view->model()->index(0, 0));
|
||||
m_project_tree_view->update();
|
||||
}
|
||||
if (Debugger::is_initialized()) {
|
||||
Debugger::the().reset_breakpoints();
|
||||
}
|
||||
}
|
||||
|
||||
Vector<String> HackStudioWidget::selected_file_names() const
|
||||
{
|
||||
Vector<String> files;
|
||||
m_project_tree_view->selection().for_each_index([&](const GUI::ModelIndex& index) {
|
||||
files.append(index.data().as_string());
|
||||
});
|
||||
return files;
|
||||
}
|
||||
|
||||
void HackStudioWidget::open_file(const String& filename)
|
||||
{
|
||||
if (!currently_open_file().is_empty()) {
|
||||
// Since the file is previously open, it should always be in m_open_files.
|
||||
ASSERT(m_open_files.find(currently_open_file()) != m_open_files.end());
|
||||
auto previous_open_project_file = m_open_files.get(currently_open_file()).value();
|
||||
|
||||
// Update the scrollbar values of the previous_open_project_file and save them to m_open_files.
|
||||
previous_open_project_file->vertical_scroll_value(current_editor().vertical_scrollbar().value());
|
||||
previous_open_project_file->horizontal_scroll_value(current_editor().horizontal_scrollbar().value());
|
||||
m_open_files.set(currently_open_file(), previous_open_project_file);
|
||||
}
|
||||
|
||||
RefPtr<ProjectFile> new_project_file = nullptr;
|
||||
if (auto it = m_open_files.find(filename); it != m_open_files.end()) {
|
||||
new_project_file = it->value;
|
||||
} else {
|
||||
new_project_file = m_project->get_file(filename);
|
||||
if (!new_project_file) {
|
||||
new_project_file = ProjectFile::construct_with_name(filename);
|
||||
}
|
||||
m_open_files.set(filename, *new_project_file);
|
||||
m_open_files_vector.append(filename);
|
||||
m_open_files_view->model()->update();
|
||||
}
|
||||
|
||||
current_editor().set_document(const_cast<GUI::TextDocument&>(new_project_file->document()));
|
||||
current_editor().set_mode(GUI::TextEditor::Editable);
|
||||
current_editor().horizontal_scrollbar().set_value(new_project_file->horizontal_scroll_value());
|
||||
current_editor().vertical_scrollbar().set_value(new_project_file->vertical_scroll_value());
|
||||
current_editor().set_editing_engine(make<GUI::RegularEditingEngine>());
|
||||
|
||||
if (filename.ends_with(".frm")) {
|
||||
set_edit_mode(EditMode::Form);
|
||||
} else {
|
||||
set_edit_mode(EditMode::Text);
|
||||
}
|
||||
|
||||
m_currently_open_file = filename;
|
||||
|
||||
String relative_file_path = m_currently_open_file;
|
||||
if (m_currently_open_file.starts_with(m_project->root_path()))
|
||||
relative_file_path = m_currently_open_file.substring(m_project->root_path().length() + 1);
|
||||
|
||||
window()->set_title(String::formatted("{} - {} - Hack Studio", relative_file_path, m_project->name()));
|
||||
m_project_tree_view->update();
|
||||
|
||||
current_editor_wrapper().filename_label().set_text(filename);
|
||||
|
||||
current_editor().set_focus(true);
|
||||
}
|
||||
|
||||
EditorWrapper& HackStudioWidget::current_editor_wrapper()
|
||||
{
|
||||
ASSERT(m_current_editor_wrapper);
|
||||
return *m_current_editor_wrapper;
|
||||
}
|
||||
|
||||
GUI::TextEditor& HackStudioWidget::current_editor()
|
||||
{
|
||||
return current_editor_wrapper().editor();
|
||||
}
|
||||
|
||||
void HackStudioWidget::set_edit_mode(EditMode mode)
|
||||
{
|
||||
if (mode == EditMode::Text) {
|
||||
m_right_hand_stack->set_active_widget(m_editors_splitter);
|
||||
} else if (mode == EditMode::Form) {
|
||||
m_right_hand_stack->set_active_widget(m_form_inner_container);
|
||||
} else if (mode == EditMode::Diff) {
|
||||
m_right_hand_stack->set_active_widget(m_diff_viewer);
|
||||
} else {
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
m_right_hand_stack->active_widget()->update();
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Menu> HackStudioWidget::create_project_tree_view_context_menu()
|
||||
{
|
||||
m_open_selected_action = create_open_selected_action();
|
||||
m_new_action = create_new_action();
|
||||
m_delete_action = create_delete_action();
|
||||
auto project_tree_view_context_menu = GUI::Menu::construct("Project Files");
|
||||
project_tree_view_context_menu->add_action(*m_open_selected_action);
|
||||
// TODO: Rename, cut, copy, duplicate with new name, show containing folder ...
|
||||
project_tree_view_context_menu->add_separator();
|
||||
project_tree_view_context_menu->add_action(*m_new_action);
|
||||
project_tree_view_context_menu->add_action(*m_delete_action);
|
||||
return project_tree_view_context_menu;
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_new_action()
|
||||
{
|
||||
return GUI::Action::create("Add new file to project...", { Mod_Ctrl, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [this](const GUI::Action&) {
|
||||
String filename;
|
||||
if (GUI::InputBox::show(filename, window(), "Enter name of new file:", "Add new file to project") != GUI::InputBox::ExecOK)
|
||||
return;
|
||||
auto file = Core::File::construct(filename);
|
||||
if (!file->open((Core::IODevice::OpenMode)(Core::IODevice::WriteOnly | Core::IODevice::MustBeNew))) {
|
||||
GUI::MessageBox::show(window(), String::formatted("Failed to create '{}'", filename), "Error", GUI::MessageBox::Type::Error);
|
||||
return;
|
||||
}
|
||||
m_project_tree_view->toggle_index(m_project_tree_view->model()->index(0, 0));
|
||||
open_file(filename);
|
||||
});
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_open_selected_action()
|
||||
{
|
||||
|
||||
auto open_selected_action = GUI::Action::create("Open", [this](const GUI::Action&) {
|
||||
auto files = selected_file_names();
|
||||
for (auto& file : files)
|
||||
open_file(file);
|
||||
});
|
||||
open_selected_action->set_enabled(true);
|
||||
return open_selected_action;
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_delete_action()
|
||||
{
|
||||
auto delete_action = GUI::CommonActions::make_delete_action([this](const GUI::Action&) {
|
||||
auto files = selected_file_names();
|
||||
if (files.is_empty())
|
||||
return;
|
||||
|
||||
String message;
|
||||
if (files.size() == 1) {
|
||||
message = String::formatted("Really remove {} from disk?", LexicalPath(files[0]).basename());
|
||||
} else {
|
||||
message = String::formatted("Really remove {} files from disk?", files.size());
|
||||
}
|
||||
|
||||
auto result = GUI::MessageBox::show(window(),
|
||||
message,
|
||||
"Confirm deletion",
|
||||
GUI::MessageBox::Type::Warning,
|
||||
GUI::MessageBox::InputType::OKCancel);
|
||||
if (result == GUI::MessageBox::ExecCancel)
|
||||
return;
|
||||
|
||||
for (auto& file : files) {
|
||||
if (1) {
|
||||
// FIXME: Remove `file` from disk
|
||||
} else {
|
||||
GUI::MessageBox::show(window(),
|
||||
String::formatted("Removing file {} from the project failed.", file),
|
||||
"Removal failed",
|
||||
GUI::MessageBox::Type::Error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
delete_action->set_enabled(false);
|
||||
return delete_action;
|
||||
}
|
||||
|
||||
void HackStudioWidget::add_new_editor(GUI::Widget& parent)
|
||||
{
|
||||
auto wrapper = EditorWrapper::construct();
|
||||
if (m_action_tab_widget) {
|
||||
parent.insert_child_before(wrapper, *m_action_tab_widget);
|
||||
} else {
|
||||
parent.add_child(wrapper);
|
||||
}
|
||||
m_current_editor_wrapper = wrapper;
|
||||
m_all_editor_wrappers.append(wrapper);
|
||||
wrapper->editor().set_focus(true);
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_switch_to_next_editor_action()
|
||||
{
|
||||
return GUI::Action::create("Switch to next editor", { Mod_Ctrl, Key_E }, [this](auto&) {
|
||||
if (m_all_editor_wrappers.size() <= 1)
|
||||
return;
|
||||
Vector<EditorWrapper*> wrappers;
|
||||
m_editors_splitter->for_each_child_of_type<EditorWrapper>([this, &wrappers](auto& child) {
|
||||
wrappers.append(&child);
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
for (size_t i = 0; i < wrappers.size(); ++i) {
|
||||
if (m_current_editor_wrapper.ptr() == wrappers[i]) {
|
||||
if (i == wrappers.size() - 1)
|
||||
wrappers[0]->editor().set_focus(true);
|
||||
else
|
||||
wrappers[i + 1]->editor().set_focus(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_switch_to_previous_editor_action()
|
||||
{
|
||||
return GUI::Action::create("Switch to previous editor", { Mod_Ctrl | Mod_Shift, Key_E }, [this](auto&) {
|
||||
if (m_all_editor_wrappers.size() <= 1)
|
||||
return;
|
||||
Vector<EditorWrapper*> wrappers;
|
||||
m_editors_splitter->for_each_child_of_type<EditorWrapper>([this, &wrappers](auto& child) {
|
||||
wrappers.append(&child);
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
for (int i = wrappers.size() - 1; i >= 0; --i) {
|
||||
if (m_current_editor_wrapper.ptr() == wrappers[i]) {
|
||||
if (i == 0)
|
||||
wrappers.last()->editor().set_focus(true);
|
||||
else
|
||||
wrappers[i - 1]->editor().set_focus(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_remove_current_editor_action()
|
||||
{
|
||||
return GUI::Action::create("Remove current editor", { Mod_Alt | Mod_Shift, Key_E }, [this](auto&) {
|
||||
if (m_all_editor_wrappers.size() <= 1)
|
||||
return;
|
||||
auto wrapper = m_current_editor_wrapper;
|
||||
m_switch_to_next_editor->activate();
|
||||
m_editors_splitter->remove_child(*wrapper);
|
||||
m_all_editor_wrappers.remove_first_matching([&wrapper](auto& entry) { return entry == wrapper.ptr(); });
|
||||
update_actions();
|
||||
});
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_open_action()
|
||||
{
|
||||
return GUI::Action::create("Open project...", { Mod_Ctrl | Mod_Shift, Key_O }, Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"), [this](auto&) {
|
||||
auto open_path = GUI::FilePicker::get_open_filepath(window(), "Open project");
|
||||
if (!open_path.has_value())
|
||||
return;
|
||||
open_project(open_path.value());
|
||||
update_actions();
|
||||
});
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_save_action()
|
||||
{
|
||||
return GUI::Action::create("Save", { Mod_Ctrl, Key_S }, Gfx::Bitmap::load_from_file("/res/icons/16x16/save.png"), [this](auto&) {
|
||||
if (m_currently_open_file.is_empty())
|
||||
return;
|
||||
|
||||
current_editor().write_to_file(m_currently_open_file);
|
||||
|
||||
if (m_git_widget->initialized())
|
||||
m_git_widget->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_remove_current_terminal_action()
|
||||
{
|
||||
return GUI::Action::create("Remove current Terminal", { Mod_Alt | Mod_Shift, Key_T }, [this](auto&) {
|
||||
auto widget = m_action_tab_widget->active_widget();
|
||||
if (!widget)
|
||||
return;
|
||||
if (!is<TerminalWrapper>(widget))
|
||||
return;
|
||||
auto& terminal = *static_cast<TerminalWrapper*>(widget);
|
||||
if (!terminal.user_spawned())
|
||||
return;
|
||||
m_action_tab_widget->remove_tab(terminal);
|
||||
update_actions();
|
||||
});
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_add_editor_action()
|
||||
{
|
||||
return GUI::Action::create("Add new editor", { Mod_Ctrl | Mod_Alt, Key_E },
|
||||
Gfx::Bitmap::load_from_file("/res/icons/16x16/app-text-editor.png"),
|
||||
[this](auto&) {
|
||||
add_new_editor(*m_editors_splitter);
|
||||
update_actions();
|
||||
});
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_add_terminal_action()
|
||||
{
|
||||
return GUI::Action::create("Add new Terminal", { Mod_Ctrl | Mod_Alt, Key_T },
|
||||
Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"),
|
||||
[this](auto&) {
|
||||
auto& terminal_wrapper = m_action_tab_widget->add_tab<TerminalWrapper>("Terminal");
|
||||
reveal_action_tab(terminal_wrapper);
|
||||
update_actions();
|
||||
terminal_wrapper.terminal().set_focus(true);
|
||||
});
|
||||
}
|
||||
|
||||
void HackStudioWidget::reveal_action_tab(GUI::Widget& widget)
|
||||
{
|
||||
if (m_action_tab_widget->min_height() < 200)
|
||||
m_action_tab_widget->set_fixed_height(200);
|
||||
m_action_tab_widget->set_active_widget(&widget);
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_debug_action()
|
||||
{
|
||||
return GUI::Action::create("Debug", Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-run.png"), [this](auto&) {
|
||||
if (!GUI::FilePicker::file_exists(get_project_executable_path())) {
|
||||
GUI::MessageBox::show(window(), String::formatted("Could not find file: {}. (did you build the project?)", get_project_executable_path()), "Error", GUI::MessageBox::Type::Error);
|
||||
return;
|
||||
}
|
||||
if (Debugger::the().session()) {
|
||||
GUI::MessageBox::show(window(), "Debugger is already running", "Error", GUI::MessageBox::Type::Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Debugger::the().set_executable_path(get_project_executable_path());
|
||||
m_debugger_thread = LibThread::Thread::construct(Debugger::start_static);
|
||||
m_debugger_thread->start();
|
||||
});
|
||||
}
|
||||
|
||||
void HackStudioWidget::initialize_debugger()
|
||||
{
|
||||
Debugger::initialize(
|
||||
m_project->root_path(),
|
||||
[this](const PtraceRegisters& regs) {
|
||||
ASSERT(Debugger::the().session());
|
||||
const auto& debug_session = *Debugger::the().session();
|
||||
auto source_position = debug_session.get_source_position(regs.eip);
|
||||
if (!source_position.has_value()) {
|
||||
dbgln("Could not find source position for address: {:p}", regs.eip);
|
||||
return Debugger::HasControlPassedToUser::No;
|
||||
}
|
||||
dbgln("Debugger stopped at source position: {}:{}", source_position.value().file_path, source_position.value().line_number);
|
||||
|
||||
Core::EventLoop::main().post_event(
|
||||
*window(),
|
||||
make<Core::DeferredInvocationEvent>(
|
||||
[this, source_position, ®s](auto&) {
|
||||
m_current_editor_in_execution = get_editor_of_file(source_position.value().file_path);
|
||||
m_current_editor_in_execution->editor().set_execution_position(source_position.value().line_number - 1);
|
||||
m_debug_info_widget->update_state(*Debugger::the().session(), regs);
|
||||
m_debug_info_widget->set_debug_actions_enabled(true);
|
||||
m_disassembly_widget->update_state(*Debugger::the().session(), regs);
|
||||
HackStudioWidget::reveal_action_tab(*m_debug_info_widget);
|
||||
}));
|
||||
Core::EventLoop::wake();
|
||||
|
||||
return Debugger::HasControlPassedToUser::Yes;
|
||||
},
|
||||
[this]() {
|
||||
Core::EventLoop::main().post_event(*window(), make<Core::DeferredInvocationEvent>([this](auto&) {
|
||||
m_debug_info_widget->set_debug_actions_enabled(false);
|
||||
if (m_current_editor_in_execution) {
|
||||
m_current_editor_in_execution->editor().clear_execution_position();
|
||||
}
|
||||
}));
|
||||
Core::EventLoop::wake();
|
||||
},
|
||||
[this]() {
|
||||
Core::EventLoop::main().post_event(*window(), make<Core::DeferredInvocationEvent>([this](auto&) {
|
||||
m_debug_info_widget->program_stopped();
|
||||
m_disassembly_widget->program_stopped();
|
||||
HackStudioWidget::hide_action_tabs();
|
||||
GUI::MessageBox::show(window(), "Program Exited", "Debugger", GUI::MessageBox::Type::Information);
|
||||
}));
|
||||
Core::EventLoop::wake();
|
||||
});
|
||||
}
|
||||
|
||||
String HackStudioWidget::get_full_path_of_serenity_source(const String& file)
|
||||
{
|
||||
auto path_parts = LexicalPath(file).parts();
|
||||
ASSERT(path_parts[0] == "..");
|
||||
path_parts.remove(0);
|
||||
StringBuilder relative_path_builder;
|
||||
relative_path_builder.join("/", path_parts);
|
||||
constexpr char SERENITY_LIBS_PREFIX[] = "/usr/src/serenity";
|
||||
LexicalPath serenity_sources_base(SERENITY_LIBS_PREFIX);
|
||||
return String::formatted("{}/{}", serenity_sources_base, relative_path_builder.to_string());
|
||||
}
|
||||
|
||||
NonnullRefPtr<EditorWrapper> HackStudioWidget::get_editor_of_file(const String& file_name)
|
||||
{
|
||||
|
||||
String file_path = file_name;
|
||||
|
||||
// TODO: We can probably do a more specific condition here, something like
|
||||
// "if (file.starts_with("../Libraries/") || file.starts_with("../AK/"))"
|
||||
if (file_name.starts_with("../")) {
|
||||
file_path = get_full_path_of_serenity_source(file_name);
|
||||
}
|
||||
|
||||
open_file(file_path);
|
||||
return current_editor_wrapper();
|
||||
}
|
||||
|
||||
String HackStudioWidget::get_project_executable_path() const
|
||||
{
|
||||
// FIXME: Dumb heuristic ahead!
|
||||
// e.g /my/project => /my/project/project
|
||||
// TODO: Perhaps a Makefile rule for getting the value of $(PROGRAM) would be better?
|
||||
return String::formatted("{}/{}", m_project->root_path(), LexicalPath(m_project->root_path()).basename());
|
||||
}
|
||||
|
||||
void HackStudioWidget::build(TerminalWrapper& wrapper)
|
||||
{
|
||||
if (m_currently_open_file.ends_with(".js"))
|
||||
wrapper.run_command(String::formatted("js -A {}", m_currently_open_file));
|
||||
else
|
||||
wrapper.run_command("make");
|
||||
}
|
||||
|
||||
void HackStudioWidget::run(TerminalWrapper& wrapper)
|
||||
{
|
||||
if (m_currently_open_file.ends_with(".js"))
|
||||
wrapper.run_command(String::formatted("js {}", m_currently_open_file));
|
||||
else
|
||||
wrapper.run_command("make run");
|
||||
}
|
||||
|
||||
void HackStudioWidget::hide_action_tabs()
|
||||
{
|
||||
m_action_tab_widget->set_fixed_height(24);
|
||||
};
|
||||
|
||||
Project& HackStudioWidget::project()
|
||||
{
|
||||
return *m_project;
|
||||
}
|
||||
|
||||
void HackStudioWidget::set_current_editor_wrapper(RefPtr<EditorWrapper> editor_wrapper)
|
||||
{
|
||||
m_current_editor_wrapper = editor_wrapper;
|
||||
}
|
||||
|
||||
void HackStudioWidget::create_project_tree_view(GUI::Widget& parent)
|
||||
{
|
||||
m_project_tree_view = parent.add<GUI::TreeView>();
|
||||
m_project_tree_view->set_model(m_project->model());
|
||||
|
||||
for (int column_index = 0; column_index < m_project->model().column_count(); ++column_index)
|
||||
m_project_tree_view->set_column_hidden(column_index, true);
|
||||
|
||||
m_project_tree_view->set_column_hidden(GUI::FileSystemModel::Column::Name, false);
|
||||
|
||||
m_project_tree_view->on_context_menu_request = [this](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
|
||||
if (index.is_valid()) {
|
||||
m_project_tree_view_context_menu->popup(event.screen_position(), m_open_selected_action);
|
||||
}
|
||||
};
|
||||
|
||||
m_project_tree_view->on_selection_change = [this] {
|
||||
m_open_selected_action->set_enabled(!m_project_tree_view->selection().is_empty());
|
||||
m_delete_action->set_enabled(!m_project_tree_view->selection().is_empty());
|
||||
};
|
||||
|
||||
m_project_tree_view->on_activation = [this](auto& index) {
|
||||
auto filename = index.data(GUI::ModelRole::Custom).to_string();
|
||||
open_file(filename);
|
||||
};
|
||||
}
|
||||
|
||||
void HackStudioWidget::create_open_files_view(GUI::Widget& parent)
|
||||
{
|
||||
m_open_files_view = parent.add<GUI::ListView>();
|
||||
auto open_files_model = GUI::ItemListModel<String>::create(m_open_files_vector);
|
||||
m_open_files_view->set_model(open_files_model);
|
||||
|
||||
m_open_files_view->on_activation = [this](auto& index) {
|
||||
open_file(index.data().to_string());
|
||||
};
|
||||
}
|
||||
|
||||
void HackStudioWidget::create_form_editor(GUI::Widget& parent)
|
||||
{
|
||||
m_form_inner_container = parent.add<GUI::Widget>();
|
||||
m_form_inner_container->set_layout<GUI::HorizontalBoxLayout>();
|
||||
auto& form_widgets_toolbar = m_form_inner_container->add<GUI::ToolBar>(Orientation::Vertical, 26);
|
||||
form_widgets_toolbar.set_fixed_width(38);
|
||||
|
||||
GUI::ActionGroup tool_actions;
|
||||
tool_actions.set_exclusive(true);
|
||||
|
||||
auto cursor_tool_action = GUI::Action::create_checkable("Cursor", Gfx::Bitmap::load_from_file("/res/icons/hackstudio/Cursor.png"), [this](auto&) {
|
||||
m_form_editor_widget->set_tool(make<CursorTool>(*m_form_editor_widget));
|
||||
});
|
||||
cursor_tool_action->set_checked(true);
|
||||
tool_actions.add_action(cursor_tool_action);
|
||||
|
||||
form_widgets_toolbar.add_action(cursor_tool_action);
|
||||
|
||||
GUI::WidgetClassRegistration::for_each([&, this](const GUI::WidgetClassRegistration& reg) {
|
||||
constexpr size_t gui_namespace_prefix_length = sizeof("GUI::") - 1;
|
||||
auto icon_path = String::formatted("/res/icons/hackstudio/G{}.png",
|
||||
reg.class_name().substring(gui_namespace_prefix_length, reg.class_name().length() - gui_namespace_prefix_length));
|
||||
if (!Core::File::exists(icon_path))
|
||||
return;
|
||||
|
||||
auto action = GUI::Action::create_checkable(reg.class_name(), Gfx::Bitmap::load_from_file(icon_path), [®, this](auto&) {
|
||||
m_form_editor_widget->set_tool(make<WidgetTool>(*m_form_editor_widget, reg));
|
||||
auto widget = reg.construct();
|
||||
m_form_editor_widget->form_widget().add_child(widget);
|
||||
widget->set_relative_rect(30, 30, 30, 30);
|
||||
m_form_editor_widget->model().update();
|
||||
});
|
||||
action->set_checked(false);
|
||||
tool_actions.add_action(action);
|
||||
form_widgets_toolbar.add_action(move(action));
|
||||
});
|
||||
|
||||
auto& form_editor_inner_splitter = m_form_inner_container->add<GUI::HorizontalSplitter>();
|
||||
|
||||
m_form_editor_widget = form_editor_inner_splitter.add<FormEditorWidget>();
|
||||
|
||||
auto& form_editing_pane_container = form_editor_inner_splitter.add<GUI::VerticalSplitter>();
|
||||
form_editing_pane_container.set_fixed_width(190);
|
||||
form_editing_pane_container.set_layout<GUI::VerticalBoxLayout>();
|
||||
|
||||
auto add_properties_pane = [&](auto& text, auto& pane_widget) {
|
||||
auto& wrapper = form_editing_pane_container.add<GUI::Widget>();
|
||||
wrapper.set_layout<GUI::VerticalBoxLayout>();
|
||||
auto& label = wrapper.add<GUI::Label>(text);
|
||||
label.set_fill_with_background_color(true);
|
||||
label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
|
||||
label.set_font(Gfx::FontDatabase::default_bold_font());
|
||||
label.set_fixed_height(16);
|
||||
wrapper.add_child(pane_widget);
|
||||
};
|
||||
|
||||
m_form_widget_tree_view = GUI::TreeView::construct();
|
||||
m_form_widget_tree_view->set_model(m_form_editor_widget->model());
|
||||
m_form_widget_tree_view->on_selection_change = [this] {
|
||||
m_form_editor_widget->selection().disable_hooks();
|
||||
m_form_editor_widget->selection().clear();
|
||||
m_form_widget_tree_view->selection().for_each_index([this](auto& index) {
|
||||
// NOTE: Make sure we don't add the FormWidget itself to the selection,
|
||||
// since that would allow you to drag-move the FormWidget.
|
||||
if (index.internal_data() != &m_form_editor_widget->form_widget())
|
||||
m_form_editor_widget->selection().add(*(GUI::Widget*)index.internal_data());
|
||||
});
|
||||
m_form_editor_widget->update();
|
||||
m_form_editor_widget->selection().enable_hooks();
|
||||
};
|
||||
|
||||
m_form_editor_widget->selection().on_add = [this](auto& widget) {
|
||||
m_form_widget_tree_view->selection().add(m_form_editor_widget->model().index_for_widget(widget));
|
||||
};
|
||||
m_form_editor_widget->selection().on_remove = [this](auto& widget) {
|
||||
m_form_widget_tree_view->selection().remove(m_form_editor_widget->model().index_for_widget(widget));
|
||||
};
|
||||
m_form_editor_widget->selection().on_clear = [this] {
|
||||
m_form_widget_tree_view->selection().clear();
|
||||
};
|
||||
|
||||
add_properties_pane("Form widget tree:", *m_form_widget_tree_view);
|
||||
add_properties_pane("Widget properties:", *GUI::TableView::construct());
|
||||
}
|
||||
|
||||
void HackStudioWidget::create_toolbar(GUI::Widget& parent)
|
||||
{
|
||||
auto& toolbar = parent.add<GUI::ToolBar>();
|
||||
toolbar.add_action(*m_new_action);
|
||||
toolbar.add_action(*m_save_action);
|
||||
toolbar.add_action(*m_delete_action);
|
||||
toolbar.add_separator();
|
||||
|
||||
toolbar.add_action(GUI::CommonActions::make_cut_action([this](auto&) { current_editor().cut_action().activate(); }));
|
||||
toolbar.add_action(GUI::CommonActions::make_copy_action([this](auto&) { current_editor().copy_action().activate(); }));
|
||||
toolbar.add_action(GUI::CommonActions::make_paste_action([this](auto&) { current_editor().paste_action().activate(); }));
|
||||
toolbar.add_separator();
|
||||
toolbar.add_action(GUI::CommonActions::make_undo_action([this](auto&) { current_editor().undo_action().activate(); }));
|
||||
toolbar.add_action(GUI::CommonActions::make_redo_action([this](auto&) { current_editor().redo_action().activate(); }));
|
||||
toolbar.add_separator();
|
||||
|
||||
toolbar.add_action(*m_build_action);
|
||||
toolbar.add_separator();
|
||||
|
||||
toolbar.add_action(*m_run_action);
|
||||
toolbar.add_action(*m_stop_action);
|
||||
toolbar.add_separator();
|
||||
|
||||
toolbar.add_action(*m_debug_action);
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_build_action()
|
||||
{
|
||||
return GUI::Action::create("Build", { Mod_Ctrl, Key_B }, Gfx::Bitmap::load_from_file("/res/icons/16x16/build.png"), [this](auto&) {
|
||||
reveal_action_tab(*m_terminal_wrapper);
|
||||
build(*m_terminal_wrapper);
|
||||
m_stop_action->set_enabled(true);
|
||||
});
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_run_action()
|
||||
{
|
||||
return GUI::Action::create("Run", { Mod_Ctrl, Key_R }, Gfx::Bitmap::load_from_file("/res/icons/16x16/program-run.png"), [this](auto&) {
|
||||
reveal_action_tab(*m_terminal_wrapper);
|
||||
run(*m_terminal_wrapper);
|
||||
m_stop_action->set_enabled(true);
|
||||
});
|
||||
}
|
||||
|
||||
void HackStudioWidget::create_action_tab(GUI::Widget& parent)
|
||||
{
|
||||
m_action_tab_widget = parent.add<GUI::TabWidget>();
|
||||
|
||||
m_action_tab_widget->set_fixed_height(24);
|
||||
m_action_tab_widget->on_change = [this](auto&) {
|
||||
on_action_tab_change();
|
||||
|
||||
static bool first_time = true;
|
||||
if (!first_time)
|
||||
m_action_tab_widget->set_fixed_height(200);
|
||||
first_time = false;
|
||||
};
|
||||
|
||||
m_find_in_files_widget = m_action_tab_widget->add_tab<FindInFilesWidget>("Find in files");
|
||||
m_terminal_wrapper = m_action_tab_widget->add_tab<TerminalWrapper>("Build", false);
|
||||
m_debug_info_widget = m_action_tab_widget->add_tab<DebugInfoWidget>("Debug");
|
||||
m_disassembly_widget = m_action_tab_widget->add_tab<DisassemblyWidget>("Disassembly");
|
||||
m_git_widget = m_action_tab_widget->add_tab<GitWidget>("Git", LexicalPath(m_project->root_path()));
|
||||
m_git_widget->set_view_diff_callback([this](const auto& original_content, const auto& diff) {
|
||||
m_diff_viewer->set_content(original_content, diff);
|
||||
set_edit_mode(EditMode::Diff);
|
||||
});
|
||||
}
|
||||
|
||||
void HackStudioWidget::create_app_menubar(GUI::MenuBar& menubar)
|
||||
{
|
||||
auto& app_menu = menubar.add_menu("Hack Studio");
|
||||
app_menu.add_action(*m_open_action);
|
||||
app_menu.add_action(*m_save_action);
|
||||
app_menu.add_separator();
|
||||
app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
|
||||
GUI::Application::the()->quit();
|
||||
}));
|
||||
}
|
||||
|
||||
void HackStudioWidget::create_project_menubar(GUI::MenuBar& menubar)
|
||||
{
|
||||
auto& project_menu = menubar.add_menu("Project");
|
||||
project_menu.add_action(*m_new_action);
|
||||
}
|
||||
|
||||
void HackStudioWidget::create_edit_menubar(GUI::MenuBar& menubar)
|
||||
{
|
||||
auto& edit_menu = menubar.add_menu("Edit");
|
||||
edit_menu.add_action(GUI::Action::create("Find in files...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"), [this](auto&) {
|
||||
reveal_action_tab(*m_find_in_files_widget);
|
||||
m_find_in_files_widget->focus_textbox_and_select_all();
|
||||
}));
|
||||
|
||||
edit_menu.add_separator();
|
||||
|
||||
auto line_wrapping_action = GUI::Action::create_checkable("Line wrapping", [this](auto& action) {
|
||||
for (auto& wrapper : m_all_editor_wrappers) {
|
||||
wrapper.editor().set_line_wrapping_enabled(action.is_checked());
|
||||
}
|
||||
});
|
||||
line_wrapping_action->set_checked(current_editor().is_line_wrapping_enabled());
|
||||
edit_menu.add_action(line_wrapping_action);
|
||||
|
||||
edit_menu.add_separator();
|
||||
|
||||
auto vim_emulation_setting_action = GUI::Action::create_checkable("Vim emulation", { Mod_Ctrl | Mod_Shift | Mod_Alt, Key_V }, [this](auto& action) {
|
||||
if (action.is_checked())
|
||||
current_editor().set_editing_engine(make<GUI::VimEditingEngine>());
|
||||
else
|
||||
current_editor().set_editing_engine(make<GUI::RegularEditingEngine>());
|
||||
});
|
||||
vim_emulation_setting_action->set_checked(false);
|
||||
edit_menu.add_action(vim_emulation_setting_action);
|
||||
}
|
||||
|
||||
void HackStudioWidget::create_build_menubar(GUI::MenuBar& menubar)
|
||||
{
|
||||
auto& build_menu = menubar.add_menu("Build");
|
||||
build_menu.add_action(*m_build_action);
|
||||
build_menu.add_separator();
|
||||
build_menu.add_action(*m_run_action);
|
||||
build_menu.add_action(*m_stop_action);
|
||||
build_menu.add_separator();
|
||||
build_menu.add_action(*m_debug_action);
|
||||
}
|
||||
|
||||
void HackStudioWidget::create_view_menubar(GUI::MenuBar& menubar)
|
||||
{
|
||||
auto hide_action_tabs_action = GUI::Action::create("Hide action tabs", { Mod_Ctrl | Mod_Shift, Key_X }, [this](auto&) {
|
||||
hide_action_tabs();
|
||||
});
|
||||
auto open_locator_action = GUI::Action::create("Open locator", { Mod_Ctrl, Key_K }, [this](auto&) {
|
||||
m_locator->open();
|
||||
});
|
||||
|
||||
auto& view_menu = menubar.add_menu("View");
|
||||
view_menu.add_action(hide_action_tabs_action);
|
||||
view_menu.add_action(open_locator_action);
|
||||
view_menu.add_separator();
|
||||
view_menu.add_action(*m_add_editor_action);
|
||||
view_menu.add_action(*m_remove_current_editor_action);
|
||||
view_menu.add_action(*m_add_terminal_action);
|
||||
view_menu.add_action(*m_remove_current_terminal_action);
|
||||
}
|
||||
|
||||
void HackStudioWidget::create_help_menubar(GUI::MenuBar& menubar)
|
||||
{
|
||||
auto& help_menu = menubar.add_menu("Help");
|
||||
help_menu.add_action(GUI::CommonActions::make_about_action("Hack Studio", GUI::Icon::default_icon("app-hack-studio"), window()));
|
||||
}
|
||||
|
||||
NonnullRefPtr<GUI::Action> HackStudioWidget::create_stop_action()
|
||||
{
|
||||
auto action = GUI::Action::create("Stop", Gfx::Bitmap::load_from_file("/res/icons/16x16/program-stop.png"), [this](auto&) {
|
||||
m_terminal_wrapper->kill_running_command();
|
||||
});
|
||||
|
||||
action->set_enabled(false);
|
||||
return action;
|
||||
}
|
||||
|
||||
void HackStudioWidget::initialize_menubar(GUI::MenuBar& menubar)
|
||||
{
|
||||
create_app_menubar(menubar);
|
||||
create_project_menubar(menubar);
|
||||
create_edit_menubar(menubar);
|
||||
create_build_menubar(menubar);
|
||||
create_view_menubar(menubar);
|
||||
create_help_menubar(menubar);
|
||||
}
|
||||
|
||||
HackStudioWidget::~HackStudioWidget()
|
||||
{
|
||||
if (!m_debugger_thread.is_null()) {
|
||||
Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::Exit);
|
||||
dbgln("Waiting for debugger thread to terminate");
|
||||
auto rc = m_debugger_thread->join();
|
||||
if (rc.is_error()) {
|
||||
warnln("pthread_join: {}", strerror(rc.error().value()));
|
||||
dbgln("error joining debugger thread");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
170
Userland/DevTools/HackStudio/HackStudioWidget.h
Normal file
170
Userland/DevTools/HackStudio/HackStudioWidget.h
Normal file
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* Copyright (c) 2020, the SerenityOS developers
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Debugger/DebugInfoWidget.h"
|
||||
#include "Debugger/DisassemblyWidget.h"
|
||||
#include "EditorWrapper.h"
|
||||
#include "FindInFilesWidget.h"
|
||||
#include "FormEditorWidget.h"
|
||||
#include "Git/DiffViewer.h"
|
||||
#include "Git/GitWidget.h"
|
||||
#include "Locator.h"
|
||||
#include "Project.h"
|
||||
#include "ProjectFile.h"
|
||||
#include "TerminalWrapper.h"
|
||||
#include <LibGUI/ScrollBar.h>
|
||||
#include <LibGUI/Splitter.h>
|
||||
#include <LibGUI/Widget.h>
|
||||
#include <LibThread/Thread.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class HackStudioWidget : public GUI::Widget {
|
||||
C_OBJECT(HackStudioWidget)
|
||||
|
||||
public:
|
||||
virtual ~HackStudioWidget() override;
|
||||
void open_file(const String& filename);
|
||||
|
||||
Vector<String> selected_file_names() const;
|
||||
|
||||
void update_actions();
|
||||
Project& project();
|
||||
GUI::TextEditor& current_editor();
|
||||
EditorWrapper& current_editor_wrapper();
|
||||
void set_current_editor_wrapper(RefPtr<EditorWrapper>);
|
||||
|
||||
String currently_open_file() const { return m_currently_open_file; }
|
||||
void initialize_menubar(GUI::MenuBar&);
|
||||
|
||||
private:
|
||||
static String get_full_path_of_serenity_source(const String& file);
|
||||
|
||||
HackStudioWidget(const String& path_to_project);
|
||||
void open_project(const String& root_path);
|
||||
|
||||
enum class EditMode {
|
||||
Text,
|
||||
Form,
|
||||
Diff,
|
||||
};
|
||||
|
||||
void set_edit_mode(EditMode);
|
||||
|
||||
NonnullRefPtr<GUI::Menu> create_project_tree_view_context_menu();
|
||||
NonnullRefPtr<GUI::Action> create_new_action();
|
||||
NonnullRefPtr<GUI::Action> create_open_selected_action();
|
||||
NonnullRefPtr<GUI::Action> create_delete_action();
|
||||
NonnullRefPtr<GUI::Action> create_switch_to_next_editor_action();
|
||||
NonnullRefPtr<GUI::Action> create_switch_to_previous_editor_action();
|
||||
NonnullRefPtr<GUI::Action> create_remove_current_editor_action();
|
||||
NonnullRefPtr<GUI::Action> create_open_action();
|
||||
NonnullRefPtr<GUI::Action> create_save_action();
|
||||
NonnullRefPtr<GUI::Action> create_add_editor_action();
|
||||
NonnullRefPtr<GUI::Action> create_add_terminal_action();
|
||||
NonnullRefPtr<GUI::Action> create_remove_current_terminal_action();
|
||||
NonnullRefPtr<GUI::Action> create_debug_action();
|
||||
NonnullRefPtr<GUI::Action> create_build_action();
|
||||
NonnullRefPtr<GUI::Action> create_run_action();
|
||||
NonnullRefPtr<GUI::Action> create_stop_action();
|
||||
|
||||
void add_new_editor(GUI::Widget& parent);
|
||||
NonnullRefPtr<EditorWrapper> get_editor_of_file(const String& file_name);
|
||||
String get_project_executable_path() const;
|
||||
|
||||
void on_action_tab_change();
|
||||
void reveal_action_tab(GUI::Widget&);
|
||||
void initialize_debugger();
|
||||
|
||||
void create_project_tree_view(GUI::Widget& parent);
|
||||
void create_open_files_view(GUI::Widget& parent);
|
||||
void create_form_editor(GUI::Widget& parent);
|
||||
void create_toolbar(GUI::Widget& parent);
|
||||
void create_action_tab(GUI::Widget& parent);
|
||||
void create_app_menubar(GUI::MenuBar&);
|
||||
void create_project_menubar(GUI::MenuBar&);
|
||||
void create_edit_menubar(GUI::MenuBar&);
|
||||
void create_build_menubar(GUI::MenuBar&);
|
||||
void create_view_menubar(GUI::MenuBar&);
|
||||
void create_help_menubar(GUI::MenuBar&);
|
||||
|
||||
void run(TerminalWrapper& wrapper);
|
||||
void build(TerminalWrapper& wrapper);
|
||||
|
||||
void hide_action_tabs();
|
||||
|
||||
NonnullRefPtrVector<EditorWrapper> m_all_editor_wrappers;
|
||||
RefPtr<EditorWrapper> m_current_editor_wrapper;
|
||||
|
||||
// FIXME: This doesn't seem compatible with multiple split editors
|
||||
String m_currently_open_file;
|
||||
|
||||
HashMap<String, NonnullRefPtr<ProjectFile>> m_open_files;
|
||||
Vector<String> m_open_files_vector; // NOTE: This contains the keys from m_open_files
|
||||
|
||||
OwnPtr<Project> m_project;
|
||||
|
||||
RefPtr<GUI::TreeView> m_project_tree_view;
|
||||
RefPtr<GUI::ListView> m_open_files_view;
|
||||
RefPtr<GUI::VerticalSplitter> m_right_hand_splitter;
|
||||
RefPtr<GUI::StackWidget> m_right_hand_stack;
|
||||
RefPtr<GUI::Splitter> m_editors_splitter;
|
||||
RefPtr<GUI::Widget> m_form_inner_container;
|
||||
RefPtr<FormEditorWidget> m_form_editor_widget;
|
||||
RefPtr<GUI::TreeView> m_form_widget_tree_view;
|
||||
RefPtr<DiffViewer> m_diff_viewer;
|
||||
RefPtr<GitWidget> m_git_widget;
|
||||
RefPtr<GUI::Menu> m_project_tree_view_context_menu;
|
||||
RefPtr<GUI::TabWidget> m_action_tab_widget;
|
||||
RefPtr<TerminalWrapper> m_terminal_wrapper;
|
||||
RefPtr<Locator> m_locator;
|
||||
RefPtr<FindInFilesWidget> m_find_in_files_widget;
|
||||
RefPtr<DebugInfoWidget> m_debug_info_widget;
|
||||
RefPtr<DisassemblyWidget> m_disassembly_widget;
|
||||
RefPtr<LibThread::Thread> m_debugger_thread;
|
||||
RefPtr<EditorWrapper> m_current_editor_in_execution;
|
||||
|
||||
RefPtr<GUI::Action> m_new_action;
|
||||
RefPtr<GUI::Action> m_open_selected_action;
|
||||
RefPtr<GUI::Action> m_delete_action;
|
||||
RefPtr<GUI::Action> m_switch_to_next_editor;
|
||||
RefPtr<GUI::Action> m_switch_to_previous_editor;
|
||||
RefPtr<GUI::Action> m_remove_current_editor_action;
|
||||
RefPtr<GUI::Action> m_open_action;
|
||||
RefPtr<GUI::Action> m_save_action;
|
||||
RefPtr<GUI::Action> m_add_editor_action;
|
||||
RefPtr<GUI::Action> m_add_terminal_action;
|
||||
RefPtr<GUI::Action> m_remove_current_terminal_action;
|
||||
RefPtr<GUI::Action> m_stop_action;
|
||||
RefPtr<GUI::Action> m_debug_action;
|
||||
RefPtr<GUI::Action> m_build_action;
|
||||
RefPtr<GUI::Action> m_run_action;
|
||||
};
|
||||
}
|
38
Userland/DevTools/HackStudio/Language.h
Normal file
38
Userland/DevTools/HackStudio/Language.h
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace HackStudio {
|
||||
enum class Language {
|
||||
Unknown,
|
||||
Cpp,
|
||||
JavaScript,
|
||||
GML,
|
||||
Ini,
|
||||
Shell,
|
||||
};
|
||||
}
|
72
Userland/DevTools/HackStudio/LanguageClient.cpp
Normal file
72
Userland/DevTools/HackStudio/LanguageClient.cpp
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "LanguageClient.h"
|
||||
#include <AK/String.h>
|
||||
#include <AK/Vector.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
void ServerConnection::handle(const Messages::LanguageClient::AutoCompleteSuggestions& message)
|
||||
{
|
||||
if (m_language_client)
|
||||
m_language_client->provide_autocomplete_suggestions(message.suggestions());
|
||||
}
|
||||
|
||||
void LanguageClient::open_file(const String& path, int fd)
|
||||
{
|
||||
m_connection.post_message(Messages::LanguageServer::FileOpened(path, fd));
|
||||
}
|
||||
|
||||
void LanguageClient::set_file_content(const String& path, const String& content)
|
||||
{
|
||||
m_connection.post_message(Messages::LanguageServer::SetFileContent(path, content));
|
||||
}
|
||||
|
||||
void LanguageClient::insert_text(const String& path, const String& text, size_t line, size_t column)
|
||||
{
|
||||
m_connection.post_message(Messages::LanguageServer::FileEditInsertText(path, text, line, column));
|
||||
}
|
||||
|
||||
void LanguageClient::remove_text(const String& path, size_t from_line, size_t from_column, size_t to_line, size_t to_column)
|
||||
{
|
||||
m_connection.post_message(Messages::LanguageServer::FileEditRemoveText(path, from_line, from_column, to_line, to_column));
|
||||
}
|
||||
|
||||
void LanguageClient::request_autocomplete(const String& path, size_t cursor_line, size_t cursor_column)
|
||||
{
|
||||
m_connection.post_message(Messages::LanguageServer::AutoCompleteSuggestions(path, cursor_line, cursor_column));
|
||||
}
|
||||
|
||||
void LanguageClient::provide_autocomplete_suggestions(const Vector<GUI::AutocompleteProvider::Entry>& suggestions)
|
||||
{
|
||||
if (on_autocomplete_suggestions)
|
||||
on_autocomplete_suggestions(suggestions);
|
||||
|
||||
// Otherwise, drop it on the floor :shrug:
|
||||
}
|
||||
|
||||
}
|
122
Userland/DevTools/HackStudio/LanguageClient.h
Normal file
122
Userland/DevTools/HackStudio/LanguageClient.h
Normal file
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AutoCompleteResponse.h"
|
||||
#include <AK/Forward.h>
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/Types.h>
|
||||
#include <LibIPC/ServerConnection.h>
|
||||
|
||||
#include <DevTools/HackStudio/LanguageServers/LanguageClientEndpoint.h>
|
||||
#include <DevTools/HackStudio/LanguageServers/LanguageServerEndpoint.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class LanguageClient;
|
||||
|
||||
class ServerConnection
|
||||
: public IPC::ServerConnection<LanguageClientEndpoint, LanguageServerEndpoint>
|
||||
, public LanguageClientEndpoint {
|
||||
public:
|
||||
ServerConnection(const StringView& socket)
|
||||
: IPC::ServerConnection<LanguageClientEndpoint, LanguageServerEndpoint>(*this, socket)
|
||||
{
|
||||
}
|
||||
|
||||
void attach(LanguageClient& client)
|
||||
{
|
||||
m_language_client = &client;
|
||||
}
|
||||
|
||||
void detach()
|
||||
{
|
||||
m_language_client = nullptr;
|
||||
}
|
||||
|
||||
virtual void handshake() override
|
||||
{
|
||||
auto response = send_sync<Messages::LanguageServer::Greet>();
|
||||
set_my_client_id(response->client_id());
|
||||
}
|
||||
|
||||
template<typename ConcreteType>
|
||||
static NonnullRefPtr<ServerConnection> get_or_create(const String& project_path)
|
||||
{
|
||||
static HashMap<String, NonnullRefPtr<ConcreteType>> s_instances_for_projects;
|
||||
auto key = LexicalPath { project_path }.string();
|
||||
if (auto instance = s_instances_for_projects.get(key); instance.has_value())
|
||||
return *instance.value();
|
||||
|
||||
auto connection = ConcreteType::construct();
|
||||
connection->handshake();
|
||||
s_instances_for_projects.set(key, *connection);
|
||||
return *connection;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void handle(const Messages::LanguageClient::AutoCompleteSuggestions&) override;
|
||||
|
||||
LanguageClient* m_language_client { nullptr };
|
||||
};
|
||||
|
||||
class LanguageClient {
|
||||
public:
|
||||
explicit LanguageClient(NonnullRefPtr<ServerConnection>&& connection)
|
||||
: m_connection(*connection)
|
||||
, m_server_connection(move(connection))
|
||||
{
|
||||
m_connection.attach(*this);
|
||||
}
|
||||
|
||||
virtual ~LanguageClient()
|
||||
{
|
||||
m_connection.detach();
|
||||
}
|
||||
|
||||
virtual void open_file(const String& path, int fd);
|
||||
virtual void set_file_content(const String& path, const String& content);
|
||||
virtual void insert_text(const String& path, const String& text, size_t line, size_t column);
|
||||
virtual void remove_text(const String& path, size_t from_line, size_t from_column, size_t to_line, size_t to_column);
|
||||
virtual void request_autocomplete(const String& path, size_t cursor_line, size_t cursor_column);
|
||||
|
||||
void provide_autocomplete_suggestions(const Vector<GUI::AutocompleteProvider::Entry>&);
|
||||
|
||||
Function<void(Vector<GUI::AutocompleteProvider::Entry>)> on_autocomplete_suggestions;
|
||||
|
||||
private:
|
||||
ServerConnection& m_connection;
|
||||
NonnullRefPtr<ServerConnection> m_server_connection;
|
||||
};
|
||||
|
||||
template<typename ServerConnectionT>
|
||||
static inline NonnullOwnPtr<LanguageClient> get_language_client(const String& project_path)
|
||||
{
|
||||
return make<LanguageClient>(ServerConnection::get_or_create<ServerConnectionT>(project_path));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
set(GENERATED_SOURCES
|
||||
../../LanguageServers/LanguageServerEndpoint.h
|
||||
../../LanguageServers/LanguageClientEndpoint.h
|
||||
)
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../LanguageClient.h"
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <DevTools/HackStudio/LanguageServers/LanguageClientEndpoint.h>
|
||||
#include <DevTools/HackStudio/LanguageServers/LanguageServerEndpoint.h>
|
||||
#include <LibIPC/ServerConnection.h>
|
||||
|
||||
#define LANGUAGE_CLIENT(namespace_, socket_name) \
|
||||
namespace namespace_ { \
|
||||
class ServerConnection : public HackStudio::ServerConnection { \
|
||||
C_OBJECT(ServerConnection) \
|
||||
private: \
|
||||
ServerConnection() \
|
||||
: HackStudio::ServerConnection("/tmp/portal/language/" #socket_name) \
|
||||
{ \
|
||||
} \
|
||||
}; \
|
||||
}
|
||||
|
||||
namespace LanguageClients {
|
||||
|
||||
LANGUAGE_CLIENT(Cpp, cpp)
|
||||
LANGUAGE_CLIENT(Shell, shell)
|
||||
|
||||
}
|
||||
|
||||
#undef LANGUAGE_CLIENT
|
|
@ -0,0 +1,5 @@
|
|||
compile_ipc(LanguageServer.ipc LanguageServerEndpoint.h)
|
||||
compile_ipc(LanguageClient.ipc LanguageClientEndpoint.h)
|
||||
|
||||
add_subdirectory(Cpp)
|
||||
add_subdirectory(Shell)
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "AutoComplete.h"
|
||||
#include <AK/HashTable.h>
|
||||
#include <LibCpp/Lexer.h>
|
||||
|
||||
// #define DEBUG_AUTOCOMPLETE
|
||||
|
||||
namespace LanguageServers::Cpp {
|
||||
|
||||
Vector<GUI::AutocompleteProvider::Entry> AutoComplete::get_suggestions(const String& code, const GUI::TextPosition& autocomplete_position)
|
||||
{
|
||||
auto lines = code.split('\n', true);
|
||||
Cpp::Lexer lexer(code);
|
||||
auto tokens = lexer.lex();
|
||||
|
||||
auto index_of_target_token = token_in_position(tokens, autocomplete_position);
|
||||
if (!index_of_target_token.has_value())
|
||||
return {};
|
||||
|
||||
auto suggestions = identifier_prefixes(lines, tokens, index_of_target_token.value());
|
||||
|
||||
#ifdef DEBUG_AUTOCOMPLETE
|
||||
for (auto& suggestion : suggestions) {
|
||||
dbgln("suggestion: {}", suggestion.completion);
|
||||
}
|
||||
#endif
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
StringView AutoComplete::text_of_token(const Vector<String>& lines, const Cpp::Token& token)
|
||||
{
|
||||
ASSERT(token.m_start.line == token.m_end.line);
|
||||
ASSERT(token.m_start.column <= token.m_end.column);
|
||||
return lines[token.m_start.line].substring_view(token.m_start.column, token.m_end.column - token.m_start.column + 1);
|
||||
}
|
||||
|
||||
Optional<size_t> AutoComplete::token_in_position(const Vector<Cpp::Token>& tokens, const GUI::TextPosition& position)
|
||||
{
|
||||
for (size_t token_index = 0; token_index < tokens.size(); ++token_index) {
|
||||
auto& token = tokens[token_index];
|
||||
if (token.m_start.line != token.m_end.line)
|
||||
continue;
|
||||
if (token.m_start.line != position.line())
|
||||
continue;
|
||||
if (token.m_start.column + 1 > position.column() || token.m_end.column + 1 < position.column())
|
||||
continue;
|
||||
return token_index;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Vector<GUI::AutocompleteProvider::Entry> AutoComplete::identifier_prefixes(const Vector<String>& lines, const Vector<Cpp::Token>& tokens, size_t target_token_index)
|
||||
{
|
||||
auto partial_input = text_of_token(lines, tokens[target_token_index]);
|
||||
Vector<GUI::AutocompleteProvider::Entry> suggestions;
|
||||
|
||||
HashTable<String> suggestions_lookup; // To avoid duplicate results
|
||||
|
||||
for (size_t i = 0; i < target_token_index; ++i) {
|
||||
auto& token = tokens[i];
|
||||
if (token.m_type != Cpp::Token::Type::Identifier)
|
||||
continue;
|
||||
auto text = text_of_token(lines, token);
|
||||
if (text.starts_with(partial_input) && suggestions_lookup.set(text) == AK::HashSetResult::InsertedNewEntry) {
|
||||
suggestions.append({ text, partial_input.length(), GUI::AutocompleteProvider::CompletionKind::Identifier });
|
||||
}
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/String.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <DevTools/HackStudio/AutoCompleteResponse.h>
|
||||
#include <LibCpp/Lexer.h>
|
||||
#include <LibGUI/TextPosition.h>
|
||||
|
||||
namespace LanguageServers::Cpp {
|
||||
|
||||
using namespace ::Cpp;
|
||||
|
||||
class AutoComplete {
|
||||
public:
|
||||
AutoComplete() = delete;
|
||||
|
||||
static Vector<GUI::AutocompleteProvider::Entry> get_suggestions(const String& code, const GUI::TextPosition& autocomplete_position);
|
||||
|
||||
private:
|
||||
static Optional<size_t> token_in_position(const Vector<Cpp::Token>&, const GUI::TextPosition&);
|
||||
static StringView text_of_token(const Vector<String>& lines, const Cpp::Token&);
|
||||
static Vector<GUI::AutocompleteProvider::Entry> identifier_prefixes(const Vector<String>& lines, const Vector<Cpp::Token>&, size_t target_token_index);
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
set(SOURCES
|
||||
ClientConnection.cpp
|
||||
main.cpp
|
||||
AutoComplete.cpp
|
||||
)
|
||||
|
||||
set(GENERATED_SOURCES
|
||||
../LanguageServerEndpoint.h
|
||||
../LanguageClientEndpoint.h)
|
||||
|
||||
serenity_bin(CppLanguageServer)
|
||||
|
||||
# We link with LibGUI because we use GUI::TextDocument to update
|
||||
# the content of files according to the edit actions we receive over IPC.
|
||||
target_link_libraries(CppLanguageServer LibIPC LibCpp LibGUI)
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "ClientConnection.h"
|
||||
#include "AutoComplete.h"
|
||||
#include <AK/HashMap.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibGUI/TextDocument.h>
|
||||
|
||||
// #define DEBUG_CPP_LANGUAGE_SERVER
|
||||
// #define DEBUG_FILE_CONTENT
|
||||
|
||||
namespace LanguageServers::Cpp {
|
||||
|
||||
static HashMap<int, RefPtr<ClientConnection>> s_connections;
|
||||
|
||||
ClientConnection::ClientConnection(NonnullRefPtr<Core::LocalSocket> socket, int client_id)
|
||||
: IPC::ClientConnection<LanguageClientEndpoint, LanguageServerEndpoint>(*this, move(socket), client_id)
|
||||
{
|
||||
s_connections.set(client_id, *this);
|
||||
}
|
||||
|
||||
ClientConnection::~ClientConnection()
|
||||
{
|
||||
}
|
||||
|
||||
void ClientConnection::die()
|
||||
{
|
||||
s_connections.remove(client_id());
|
||||
exit(0);
|
||||
}
|
||||
|
||||
OwnPtr<Messages::LanguageServer::GreetResponse> ClientConnection::handle(const Messages::LanguageServer::Greet&)
|
||||
{
|
||||
return make<Messages::LanguageServer::GreetResponse>(client_id());
|
||||
}
|
||||
|
||||
class DefaultDocumentClient final : public GUI::TextDocument::Client {
|
||||
public:
|
||||
virtual ~DefaultDocumentClient() override = default;
|
||||
virtual void document_did_append_line() override {};
|
||||
virtual void document_did_insert_line(size_t) override {};
|
||||
virtual void document_did_remove_line(size_t) override {};
|
||||
virtual void document_did_remove_all_lines() override {};
|
||||
virtual void document_did_change() override {};
|
||||
virtual void document_did_set_text() override {};
|
||||
virtual void document_did_set_cursor(const GUI::TextPosition&) override {};
|
||||
|
||||
virtual bool is_automatic_indentation_enabled() const override { return false; }
|
||||
virtual int soft_tab_width() const override { return 4; }
|
||||
};
|
||||
|
||||
static DefaultDocumentClient s_default_document_client;
|
||||
|
||||
void ClientConnection::handle(const Messages::LanguageServer::FileOpened& message)
|
||||
{
|
||||
auto file = Core::File::construct(this);
|
||||
if (!file->open(message.file().fd(), Core::IODevice::ReadOnly, Core::File::ShouldCloseFileDescriptor::Yes)) {
|
||||
errno = file->error();
|
||||
perror("open");
|
||||
dbgln("Failed to open project file");
|
||||
return;
|
||||
}
|
||||
auto content = file->read_all();
|
||||
StringView content_view(content);
|
||||
auto document = GUI::TextDocument::create(&s_default_document_client);
|
||||
document->set_text(content_view);
|
||||
m_open_files.set(message.file_name(), document);
|
||||
#ifdef DEBUG_FILE_CONTENT
|
||||
dbg() << document->text();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ClientConnection::handle(const Messages::LanguageServer::FileEditInsertText& message)
|
||||
{
|
||||
#ifdef DEBUG_CPP_LANGUAGE_SERVER
|
||||
dbgln("InsertText for file: {}", message.file_name());
|
||||
dbgln("Text: {}", message.text());
|
||||
dbgln("[{}:{}]", message.start_line(), message.start_column());
|
||||
#endif
|
||||
auto document = document_for(message.file_name());
|
||||
if (!document) {
|
||||
dbgln("file {} has not been opened", message.file_name());
|
||||
return;
|
||||
}
|
||||
GUI::TextPosition start_position { (size_t)message.start_line(), (size_t)message.start_column() };
|
||||
document->insert_at(start_position, message.text(), &s_default_document_client);
|
||||
#ifdef DEBUG_FILE_CONTENT
|
||||
dbgln("{}", document->text());
|
||||
#endif
|
||||
}
|
||||
|
||||
void ClientConnection::handle(const Messages::LanguageServer::FileEditRemoveText& message)
|
||||
{
|
||||
#ifdef DEBUG_CPP_LANGUAGE_SERVER
|
||||
dbgln("RemoveText for file: {}", message.file_name());
|
||||
dbgln("[{}:{} - {}:{}]", message.start_line(), message.start_column(), message.end_line(), message.end_column());
|
||||
#endif
|
||||
auto document = document_for(message.file_name());
|
||||
if (!document) {
|
||||
dbgln("file {} has not been opened", message.file_name());
|
||||
return;
|
||||
}
|
||||
GUI::TextPosition start_position { (size_t)message.start_line(), (size_t)message.start_column() };
|
||||
GUI::TextRange range {
|
||||
GUI::TextPosition { (size_t)message.start_line(),
|
||||
(size_t)message.start_column() },
|
||||
GUI::TextPosition { (size_t)message.end_line(),
|
||||
(size_t)message.end_column() }
|
||||
};
|
||||
|
||||
document->remove(range);
|
||||
#ifdef DEBUG_FILE_CONTENT
|
||||
dbgln("{}", document->text());
|
||||
#endif
|
||||
}
|
||||
|
||||
void ClientConnection::handle(const Messages::LanguageServer::AutoCompleteSuggestions& message)
|
||||
{
|
||||
#ifdef DEBUG_CPP_LANGUAGE_SERVER
|
||||
dbgln("AutoCompleteSuggestions for: {} {}:{}", message.file_name(), message.cursor_line(), message.cursor_column());
|
||||
#endif
|
||||
|
||||
auto document = document_for(message.file_name());
|
||||
if (!document) {
|
||||
dbgln("file {} has not been opened", message.file_name());
|
||||
return;
|
||||
}
|
||||
|
||||
auto suggestions = AutoComplete::get_suggestions(document->text(), { (size_t)message.cursor_line(), (size_t)max(message.cursor_column(), message.cursor_column() - 1) });
|
||||
post_message(Messages::LanguageClient::AutoCompleteSuggestions(move(suggestions)));
|
||||
}
|
||||
|
||||
RefPtr<GUI::TextDocument> ClientConnection::document_for(const String& file_name)
|
||||
{
|
||||
auto document_optional = m_open_files.get(file_name);
|
||||
if (!document_optional.has_value())
|
||||
return nullptr;
|
||||
|
||||
return document_optional.value();
|
||||
}
|
||||
|
||||
void ClientConnection::handle(const Messages::LanguageServer::SetFileContent& message)
|
||||
{
|
||||
auto document = document_for(message.file_name());
|
||||
if (!document) {
|
||||
dbgln("file {} has not been opened", message.file_name());
|
||||
return;
|
||||
}
|
||||
auto content = message.content();
|
||||
document->set_text(content.view());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <DevTools/HackStudio/AutoCompleteResponse.h>
|
||||
#include <LibGUI/TextDocument.h>
|
||||
#include <LibIPC/ClientConnection.h>
|
||||
|
||||
#include <DevTools/HackStudio/LanguageServers/LanguageClientEndpoint.h>
|
||||
#include <DevTools/HackStudio/LanguageServers/LanguageServerEndpoint.h>
|
||||
|
||||
namespace LanguageServers::Cpp {
|
||||
|
||||
class ClientConnection final
|
||||
: public IPC::ClientConnection<LanguageClientEndpoint, LanguageServerEndpoint>
|
||||
, public LanguageServerEndpoint {
|
||||
C_OBJECT(ClientConnection);
|
||||
|
||||
public:
|
||||
explicit ClientConnection(NonnullRefPtr<Core::LocalSocket>, int client_id);
|
||||
~ClientConnection() override;
|
||||
|
||||
virtual void die() override;
|
||||
|
||||
private:
|
||||
virtual OwnPtr<Messages::LanguageServer::GreetResponse> handle(const Messages::LanguageServer::Greet&) override;
|
||||
virtual void handle(const Messages::LanguageServer::FileOpened&) override;
|
||||
virtual void handle(const Messages::LanguageServer::FileEditInsertText&) override;
|
||||
virtual void handle(const Messages::LanguageServer::FileEditRemoveText&) override;
|
||||
virtual void handle(const Messages::LanguageServer::SetFileContent&) override;
|
||||
virtual void handle(const Messages::LanguageServer::AutoCompleteSuggestions&) override;
|
||||
|
||||
RefPtr<GUI::TextDocument> document_for(const String& file_name);
|
||||
|
||||
HashMap<String, NonnullRefPtr<GUI::TextDocument>> m_open_files;
|
||||
};
|
||||
|
||||
}
|
55
Userland/DevTools/HackStudio/LanguageServers/Cpp/main.cpp
Normal file
55
Userland/DevTools/HackStudio/LanguageServers/Cpp/main.cpp
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <DevTools/HackStudio/LanguageServers/Cpp/ClientConnection.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibCore/LocalServer.h>
|
||||
#include <LibIPC/ClientConnection.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(int, char**)
|
||||
{
|
||||
Core::EventLoop event_loop;
|
||||
if (pledge("stdio unix recvfd", nullptr) < 0) {
|
||||
perror("pledge");
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto socket = Core::LocalSocket::take_over_accepted_socket_from_system_server();
|
||||
IPC::new_client_connection<LanguageServers::Cpp::ClientConnection>(socket.release_nonnull(), 1);
|
||||
if (pledge("stdio recvfd", nullptr) < 0) {
|
||||
perror("pledge");
|
||||
return 1;
|
||||
}
|
||||
if (unveil(nullptr, nullptr) < 0) {
|
||||
perror("unveil");
|
||||
return 1;
|
||||
}
|
||||
return event_loop.exec();
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
endpoint LanguageClient = 8002
|
||||
{
|
||||
AutoCompleteSuggestions(Vector<GUI::AutocompleteProvider::Entry> suggestions) =|
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
endpoint LanguageServer = 8001
|
||||
{
|
||||
Greet() => (i32 client_id)
|
||||
|
||||
FileOpened(String file_name, IPC::File file) =|
|
||||
FileEditInsertText(String file_name, String text, i32 start_line, i32 start_column) =|
|
||||
FileEditRemoveText(String file_name, i32 start_line, i32 start_column, i32 end_line, i32 end_column) =|
|
||||
SetFileContent(String file_name, String content) =|
|
||||
|
||||
AutoCompleteSuggestions(String file_name, i32 cursor_line, i32 cursor_column) =|
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "AutoComplete.h"
|
||||
#include <AK/HashTable.h>
|
||||
#include <LibLine/SuggestionManager.h>
|
||||
#include <Shell/AST.h>
|
||||
#include <Shell/Parser.h>
|
||||
#include <Shell/Shell.h>
|
||||
|
||||
// #define DEBUG_AUTOCOMPLETE
|
||||
|
||||
namespace LanguageServers::Shell {
|
||||
|
||||
Vector<GUI::AutocompleteProvider::Entry> AutoComplete::get_suggestions(const String& code, size_t offset)
|
||||
{
|
||||
// FIXME: No need to reparse this every time!
|
||||
auto ast = ::Shell::Parser { code }.parse();
|
||||
if (!ast)
|
||||
return {};
|
||||
|
||||
#ifdef DEBUG_AUTOCOMPLETE
|
||||
dbgln("Complete '{}'", code);
|
||||
ast->dump(1);
|
||||
dbgln("At offset {}", offset);
|
||||
#endif
|
||||
|
||||
auto result = ast->complete_for_editor(m_shell, offset);
|
||||
Vector<GUI::AutocompleteProvider::Entry> completions;
|
||||
for (auto& entry : result) {
|
||||
#ifdef DEBUG_AUTOCOMPLETE
|
||||
dbgln("Suggestion: '{}' starting at {}", entry.text_string, entry.input_offset);
|
||||
#endif
|
||||
completions.append({ entry.text_string, entry.input_offset });
|
||||
}
|
||||
|
||||
return completions;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/String.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <DevTools/HackStudio/AutoCompleteResponse.h>
|
||||
#include <LibGUI/TextPosition.h>
|
||||
#include <Shell/Shell.h>
|
||||
|
||||
namespace LanguageServers::Shell {
|
||||
|
||||
class AutoComplete {
|
||||
public:
|
||||
AutoComplete()
|
||||
: m_shell(::Shell::Shell::construct())
|
||||
{
|
||||
}
|
||||
|
||||
Vector<GUI::AutocompleteProvider::Entry> get_suggestions(const String& code, size_t autocomplete_position);
|
||||
|
||||
private:
|
||||
NonnullRefPtr<::Shell::Shell> m_shell;
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
set(SOURCES
|
||||
ClientConnection.cpp
|
||||
main.cpp
|
||||
AutoComplete.cpp
|
||||
)
|
||||
|
||||
set(GENERATED_SOURCES
|
||||
../LanguageServerEndpoint.h
|
||||
../LanguageClientEndpoint.h)
|
||||
|
||||
serenity_bin(ShellLanguageServer)
|
||||
|
||||
# We link with LibGUI because we use GUI::TextDocument to update
|
||||
# the content of files according to the edit actions we receive over IPC.
|
||||
target_link_libraries(ShellLanguageServer LibIPC LibShell LibGUI)
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "ClientConnection.h"
|
||||
#include "AutoComplete.h"
|
||||
#include <AK/HashMap.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibGUI/TextDocument.h>
|
||||
|
||||
// #define DEBUG_SH_LANGUAGE_SERVER
|
||||
// #define DEBUG_FILE_CONTENT
|
||||
|
||||
namespace LanguageServers::Shell {
|
||||
|
||||
static HashMap<int, RefPtr<ClientConnection>> s_connections;
|
||||
|
||||
ClientConnection::ClientConnection(NonnullRefPtr<Core::LocalSocket> socket, int client_id)
|
||||
: IPC::ClientConnection<LanguageClientEndpoint, LanguageServerEndpoint>(*this, move(socket), client_id)
|
||||
{
|
||||
s_connections.set(client_id, *this);
|
||||
}
|
||||
|
||||
ClientConnection::~ClientConnection()
|
||||
{
|
||||
}
|
||||
|
||||
void ClientConnection::die()
|
||||
{
|
||||
s_connections.remove(client_id());
|
||||
exit(0);
|
||||
}
|
||||
|
||||
OwnPtr<Messages::LanguageServer::GreetResponse> ClientConnection::handle(const Messages::LanguageServer::Greet&)
|
||||
{
|
||||
return make<Messages::LanguageServer::GreetResponse>(client_id());
|
||||
}
|
||||
|
||||
class DefaultDocumentClient final : public GUI::TextDocument::Client {
|
||||
public:
|
||||
virtual ~DefaultDocumentClient() override = default;
|
||||
virtual void document_did_append_line() override {};
|
||||
virtual void document_did_insert_line(size_t) override {};
|
||||
virtual void document_did_remove_line(size_t) override {};
|
||||
virtual void document_did_remove_all_lines() override {};
|
||||
virtual void document_did_change() override {};
|
||||
virtual void document_did_set_text() override {};
|
||||
virtual void document_did_set_cursor(const GUI::TextPosition&) override {};
|
||||
|
||||
virtual bool is_automatic_indentation_enabled() const override { return false; }
|
||||
virtual int soft_tab_width() const override { return 4; }
|
||||
};
|
||||
|
||||
static DefaultDocumentClient s_default_document_client;
|
||||
|
||||
void ClientConnection::handle(const Messages::LanguageServer::FileOpened& message)
|
||||
{
|
||||
auto file = Core::File::construct(this);
|
||||
if (!file->open(message.file().fd(), Core::IODevice::ReadOnly, Core::File::ShouldCloseFileDescriptor::Yes)) {
|
||||
errno = file->error();
|
||||
perror("open");
|
||||
dbgln("Failed to open project file");
|
||||
return;
|
||||
}
|
||||
auto content = file->read_all();
|
||||
StringView content_view(content);
|
||||
auto document = GUI::TextDocument::create(&s_default_document_client);
|
||||
document->set_text(content_view);
|
||||
m_open_files.set(message.file_name(), document);
|
||||
#ifdef DEBUG_FILE_CONTENT
|
||||
dbg() << document->text();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ClientConnection::handle(const Messages::LanguageServer::FileEditInsertText& message)
|
||||
{
|
||||
#ifdef DEBUG_SH_LANGUAGE_SERVER
|
||||
dbgln("InsertText for file: {}", message.file_name());
|
||||
dbgln("Text: {}", message.text());
|
||||
dbgln("[{}:{}]", message.start_line(), message.start_column());
|
||||
#endif
|
||||
auto document = document_for(message.file_name());
|
||||
if (!document) {
|
||||
dbgln("file {} has not been opened", message.file_name());
|
||||
return;
|
||||
}
|
||||
GUI::TextPosition start_position { (size_t)message.start_line(), (size_t)message.start_column() };
|
||||
document->insert_at(start_position, message.text(), &s_default_document_client);
|
||||
#ifdef DEBUG_FILE_CONTENT
|
||||
dbgln("{}", document->text());
|
||||
#endif
|
||||
}
|
||||
|
||||
void ClientConnection::handle(const Messages::LanguageServer::FileEditRemoveText& message)
|
||||
{
|
||||
#ifdef DEBUG_SH_LANGUAGE_SERVER
|
||||
dbgln("RemoveText for file: {}", message.file_name());
|
||||
dbgln("[{}:{} - {}:{}]", message.start_line(), message.start_column(), message.end_line(), message.end_column());
|
||||
#endif
|
||||
auto document = document_for(message.file_name());
|
||||
if (!document) {
|
||||
dbgln("file {} has not been opened", message.file_name());
|
||||
return;
|
||||
}
|
||||
GUI::TextPosition start_position { (size_t)message.start_line(), (size_t)message.start_column() };
|
||||
GUI::TextRange range {
|
||||
GUI::TextPosition { (size_t)message.start_line(),
|
||||
(size_t)message.start_column() },
|
||||
GUI::TextPosition { (size_t)message.end_line(),
|
||||
(size_t)message.end_column() }
|
||||
};
|
||||
|
||||
document->remove(range);
|
||||
#ifdef DEBUG_FILE_CONTENT
|
||||
dbg() << document->text();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ClientConnection::handle(const Messages::LanguageServer::AutoCompleteSuggestions& message)
|
||||
{
|
||||
#ifdef DEBUG_SH_LANGUAGE_SERVER
|
||||
dbgln("AutoCompleteSuggestions for: {} {}:{}", message.file_name(), message.cursor_line(), message.cursor_column());
|
||||
#endif
|
||||
|
||||
auto document = document_for(message.file_name());
|
||||
if (!document) {
|
||||
dbgln("file {} has not been opened", message.file_name());
|
||||
return;
|
||||
}
|
||||
|
||||
auto& lines = document->lines();
|
||||
size_t offset = 0;
|
||||
|
||||
if (message.cursor_line() > 0) {
|
||||
for (auto i = 0; i < message.cursor_line(); ++i)
|
||||
offset += lines[i].length() + 1;
|
||||
}
|
||||
offset += message.cursor_column();
|
||||
|
||||
auto suggestions = m_autocomplete.get_suggestions(document->text(), offset);
|
||||
post_message(Messages::LanguageClient::AutoCompleteSuggestions(move(suggestions)));
|
||||
}
|
||||
|
||||
RefPtr<GUI::TextDocument> ClientConnection::document_for(const String& file_name)
|
||||
{
|
||||
auto document_optional = m_open_files.get(file_name);
|
||||
if (!document_optional.has_value())
|
||||
return nullptr;
|
||||
|
||||
return document_optional.value();
|
||||
}
|
||||
|
||||
void ClientConnection::handle(const Messages::LanguageServer::SetFileContent& message)
|
||||
{
|
||||
auto document = document_for(message.file_name());
|
||||
if (!document) {
|
||||
dbgln("file {} has not been opened", message.file_name());
|
||||
return;
|
||||
}
|
||||
auto content = message.content();
|
||||
document->set_text(content.view());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AutoComplete.h"
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <DevTools/HackStudio/AutoCompleteResponse.h>
|
||||
#include <LibGUI/TextDocument.h>
|
||||
#include <LibIPC/ClientConnection.h>
|
||||
|
||||
#include <DevTools/HackStudio/LanguageServers/LanguageClientEndpoint.h>
|
||||
#include <DevTools/HackStudio/LanguageServers/LanguageServerEndpoint.h>
|
||||
|
||||
namespace LanguageServers::Shell {
|
||||
|
||||
class ClientConnection final
|
||||
: public IPC::ClientConnection<LanguageClientEndpoint, LanguageServerEndpoint>
|
||||
, public LanguageServerEndpoint {
|
||||
C_OBJECT(ClientConnection);
|
||||
|
||||
public:
|
||||
explicit ClientConnection(NonnullRefPtr<Core::LocalSocket>, int client_id);
|
||||
~ClientConnection() override;
|
||||
|
||||
virtual void die() override;
|
||||
|
||||
private:
|
||||
virtual OwnPtr<Messages::LanguageServer::GreetResponse> handle(const Messages::LanguageServer::Greet&) override;
|
||||
virtual void handle(const Messages::LanguageServer::FileOpened&) override;
|
||||
virtual void handle(const Messages::LanguageServer::FileEditInsertText&) override;
|
||||
virtual void handle(const Messages::LanguageServer::FileEditRemoveText&) override;
|
||||
virtual void handle(const Messages::LanguageServer::SetFileContent&) override;
|
||||
virtual void handle(const Messages::LanguageServer::AutoCompleteSuggestions&) override;
|
||||
|
||||
RefPtr<GUI::TextDocument> document_for(const String& file_name);
|
||||
|
||||
HashMap<String, NonnullRefPtr<GUI::TextDocument>> m_open_files;
|
||||
|
||||
AutoComplete m_autocomplete;
|
||||
};
|
||||
|
||||
}
|
63
Userland/DevTools/HackStudio/LanguageServers/Shell/main.cpp
Normal file
63
Userland/DevTools/HackStudio/LanguageServers/Shell/main.cpp
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <DevTools/HackStudio/LanguageServers/Shell/ClientConnection.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibCore/LocalServer.h>
|
||||
#include <LibIPC/ClientConnection.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(int, char**)
|
||||
{
|
||||
Core::EventLoop event_loop;
|
||||
if (pledge("stdio unix rpath recvfd", nullptr) < 0) {
|
||||
perror("pledge");
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto socket = Core::LocalSocket::take_over_accepted_socket_from_system_server();
|
||||
IPC::new_client_connection<LanguageServers::Shell::ClientConnection>(socket.release_nonnull(), 1);
|
||||
if (pledge("stdio rpath recvfd", nullptr) < 0) {
|
||||
perror("pledge");
|
||||
return 1;
|
||||
}
|
||||
if (unveil("/etc/passwd", "r") < 0) {
|
||||
perror("unveil");
|
||||
return 1;
|
||||
}
|
||||
if (unveil("/", "b") < 0) {
|
||||
perror("unveil");
|
||||
return 1;
|
||||
}
|
||||
if (unveil(nullptr, nullptr) < 0) {
|
||||
perror("unveil");
|
||||
return 1;
|
||||
}
|
||||
return event_loop.exec();
|
||||
}
|
178
Userland/DevTools/HackStudio/Locator.cpp
Normal file
178
Userland/DevTools/HackStudio/Locator.cpp
Normal file
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "Locator.h"
|
||||
#include "HackStudio.h"
|
||||
#include "Project.h"
|
||||
#include <LibGUI/BoxLayout.h>
|
||||
#include <LibGUI/FileIconProvider.h>
|
||||
#include <LibGUI/TableView.h>
|
||||
#include <LibGUI/TextBox.h>
|
||||
#include <LibGUI/Window.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class LocatorSuggestionModel final : public GUI::Model {
|
||||
public:
|
||||
explicit LocatorSuggestionModel(Vector<String>&& suggestions)
|
||||
: m_suggestions(move(suggestions))
|
||||
{
|
||||
}
|
||||
|
||||
enum Column {
|
||||
Icon,
|
||||
Name,
|
||||
__Column_Count,
|
||||
};
|
||||
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_suggestions.size(); }
|
||||
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Column_Count; }
|
||||
virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override
|
||||
{
|
||||
auto& suggestion = m_suggestions.at(index.row());
|
||||
if (role == GUI::ModelRole::Display) {
|
||||
if (index.column() == Column::Name)
|
||||
return suggestion;
|
||||
if (index.column() == Column::Icon)
|
||||
return GUI::FileIconProvider::icon_for_path(suggestion);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
virtual void update() override {};
|
||||
|
||||
private:
|
||||
Vector<String> m_suggestions;
|
||||
};
|
||||
|
||||
Locator::Locator()
|
||||
{
|
||||
set_layout<GUI::VerticalBoxLayout>();
|
||||
set_fixed_height(20);
|
||||
m_textbox = add<GUI::TextBox>();
|
||||
m_textbox->on_change = [this] {
|
||||
update_suggestions();
|
||||
};
|
||||
m_textbox->on_escape_pressed = [this] {
|
||||
m_popup_window->hide();
|
||||
};
|
||||
m_textbox->on_up_pressed = [this] {
|
||||
GUI::ModelIndex new_index = m_suggestion_view->selection().first();
|
||||
if (new_index.is_valid())
|
||||
new_index = m_suggestion_view->model()->index(new_index.row() - 1);
|
||||
else
|
||||
new_index = m_suggestion_view->model()->index(0);
|
||||
|
||||
if (m_suggestion_view->model()->is_valid(new_index)) {
|
||||
m_suggestion_view->selection().set(new_index);
|
||||
m_suggestion_view->scroll_into_view(new_index, Orientation::Vertical);
|
||||
}
|
||||
};
|
||||
m_textbox->on_down_pressed = [this] {
|
||||
GUI::ModelIndex new_index = m_suggestion_view->selection().first();
|
||||
if (new_index.is_valid())
|
||||
new_index = m_suggestion_view->model()->index(new_index.row() + 1);
|
||||
else
|
||||
new_index = m_suggestion_view->model()->index(0);
|
||||
|
||||
if (m_suggestion_view->model()->is_valid(new_index)) {
|
||||
m_suggestion_view->selection().set(new_index);
|
||||
m_suggestion_view->scroll_into_view(new_index, Orientation::Vertical);
|
||||
}
|
||||
};
|
||||
|
||||
m_textbox->on_return_pressed = [this] {
|
||||
auto selected_index = m_suggestion_view->selection().first();
|
||||
if (!selected_index.is_valid())
|
||||
return;
|
||||
open_suggestion(selected_index);
|
||||
};
|
||||
|
||||
m_popup_window = GUI::Window::construct();
|
||||
// FIXME: This is obviously not a tooltip window, but it's the closest thing to what we want atm.
|
||||
m_popup_window->set_window_type(GUI::WindowType::Tooltip);
|
||||
m_popup_window->set_rect(0, 0, 500, 200);
|
||||
|
||||
m_suggestion_view = m_popup_window->set_main_widget<GUI::TableView>();
|
||||
m_suggestion_view->set_column_headers_visible(false);
|
||||
|
||||
m_suggestion_view->on_activation = [this](auto& index) {
|
||||
open_suggestion(index);
|
||||
};
|
||||
}
|
||||
|
||||
Locator::~Locator()
|
||||
{
|
||||
}
|
||||
|
||||
void Locator::open_suggestion(const GUI::ModelIndex& index)
|
||||
{
|
||||
auto filename_index = m_suggestion_view->model()->index(index.row(), LocatorSuggestionModel::Column::Name);
|
||||
auto filename = filename_index.data().to_string();
|
||||
open_file(filename);
|
||||
close();
|
||||
}
|
||||
|
||||
void Locator::open()
|
||||
{
|
||||
m_textbox->set_focus(true);
|
||||
if (!m_textbox->text().is_empty()) {
|
||||
m_textbox->select_all();
|
||||
m_popup_window->show();
|
||||
}
|
||||
}
|
||||
|
||||
void Locator::close()
|
||||
{
|
||||
m_popup_window->hide();
|
||||
}
|
||||
|
||||
void Locator::update_suggestions()
|
||||
{
|
||||
auto typed_text = m_textbox->text();
|
||||
Vector<String> suggestions;
|
||||
project().for_each_text_file([&](auto& file) {
|
||||
if (file.name().contains(typed_text, CaseSensitivity::CaseInsensitive))
|
||||
suggestions.append(file.name());
|
||||
});
|
||||
dbgln("I have {} suggestion(s):", suggestions.size());
|
||||
for (auto& s : suggestions) {
|
||||
dbgln(" {}", s);
|
||||
}
|
||||
|
||||
bool has_suggestions = !suggestions.is_empty();
|
||||
|
||||
m_suggestion_view->set_model(adopt(*new LocatorSuggestionModel(move(suggestions))));
|
||||
|
||||
if (!has_suggestions)
|
||||
m_suggestion_view->selection().clear();
|
||||
else
|
||||
m_suggestion_view->selection().set(m_suggestion_view->model()->index(0));
|
||||
|
||||
m_popup_window->move_to(screen_relative_rect().top_left().translated(0, -m_popup_window->height()));
|
||||
dbgln("Popup rect: {}", m_popup_window->rect());
|
||||
m_popup_window->show();
|
||||
}
|
||||
|
||||
}
|
52
Userland/DevTools/HackStudio/Locator.h
Normal file
52
Userland/DevTools/HackStudio/Locator.h
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibGUI/Widget.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class Locator final : public GUI::Widget {
|
||||
C_OBJECT(Locator)
|
||||
public:
|
||||
virtual ~Locator() override;
|
||||
|
||||
void open();
|
||||
void close();
|
||||
|
||||
private:
|
||||
void update_suggestions();
|
||||
void open_suggestion(const GUI::ModelIndex&);
|
||||
|
||||
Locator();
|
||||
|
||||
RefPtr<GUI::TextBox> m_textbox;
|
||||
RefPtr<GUI::Window> m_popup_window;
|
||||
RefPtr<GUI::TableView> m_suggestion_view;
|
||||
};
|
||||
|
||||
}
|
84
Userland/DevTools/HackStudio/Project.cpp
Normal file
84
Userland/DevTools/HackStudio/Project.cpp
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "Project.h"
|
||||
#include "HackStudio.h"
|
||||
#include <LibCore/File.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
Project::Project(const String& root_path)
|
||||
: m_root_path(root_path)
|
||||
{
|
||||
m_model = GUI::FileSystemModel::create(root_path, GUI::FileSystemModel::Mode::FilesAndDirectories);
|
||||
}
|
||||
|
||||
Project::~Project()
|
||||
{
|
||||
}
|
||||
|
||||
OwnPtr<Project> Project::open_with_root_path(const String& root_path)
|
||||
{
|
||||
if (!Core::File::is_directory(root_path))
|
||||
return {};
|
||||
return adopt_own(*new Project(root_path));
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
static void traverse_model(const GUI::FileSystemModel& model, const GUI::ModelIndex& index, Callback callback)
|
||||
{
|
||||
if (index.is_valid())
|
||||
callback(index);
|
||||
auto row_count = model.row_count(index);
|
||||
if (!row_count)
|
||||
return;
|
||||
for (int row = 0; row < row_count; ++row) {
|
||||
auto child_index = model.index(row, GUI::FileSystemModel::Column::Name, index);
|
||||
traverse_model(model, child_index, callback);
|
||||
}
|
||||
}
|
||||
|
||||
void Project::for_each_text_file(Function<void(const ProjectFile&)> callback) const
|
||||
{
|
||||
traverse_model(model(), {}, [&](auto& index) {
|
||||
auto file = get_file(model().full_path(index));
|
||||
if (file)
|
||||
callback(*file);
|
||||
});
|
||||
}
|
||||
|
||||
RefPtr<ProjectFile> Project::get_file(const String& path) const
|
||||
{
|
||||
for (auto& file : m_files) {
|
||||
if (file.name() == path)
|
||||
return file;
|
||||
}
|
||||
auto file = ProjectFile::construct_with_name(path);
|
||||
m_files.append(file);
|
||||
return file;
|
||||
}
|
||||
|
||||
}
|
64
Userland/DevTools/HackStudio/Project.h
Normal file
64
Userland/DevTools/HackStudio/Project.h
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ProjectFile.h"
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/Noncopyable.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <LibGUI/FileSystemModel.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class Project {
|
||||
AK_MAKE_NONCOPYABLE(Project);
|
||||
AK_MAKE_NONMOVABLE(Project);
|
||||
|
||||
public:
|
||||
~Project();
|
||||
|
||||
static OwnPtr<Project> open_with_root_path(const String& root_path);
|
||||
|
||||
GUI::FileSystemModel& model() { return *m_model; }
|
||||
const GUI::FileSystemModel& model() const { return *m_model; }
|
||||
String name() const { return LexicalPath(m_root_path).basename(); }
|
||||
String root_path() const { return m_root_path; }
|
||||
|
||||
RefPtr<ProjectFile> get_file(const String& path) const;
|
||||
|
||||
void for_each_text_file(Function<void(const ProjectFile&)>) const;
|
||||
|
||||
private:
|
||||
explicit Project(const String& root_path);
|
||||
|
||||
RefPtr<GUI::FileSystemModel> m_model;
|
||||
mutable NonnullRefPtrVector<ProjectFile> m_files;
|
||||
|
||||
String m_root_path;
|
||||
};
|
||||
|
||||
}
|
74
Userland/DevTools/HackStudio/ProjectFile.cpp
Normal file
74
Userland/DevTools/HackStudio/ProjectFile.cpp
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "ProjectFile.h"
|
||||
#include <LibCore/File.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
ProjectFile::ProjectFile(const String& name)
|
||||
: m_name(name)
|
||||
{
|
||||
}
|
||||
|
||||
GUI::TextDocument& ProjectFile::document() const
|
||||
{
|
||||
if (!m_document) {
|
||||
m_document = CodeDocument::create(m_name);
|
||||
auto file_or_error = Core::File::open(m_name, Core::File::ReadOnly);
|
||||
if (file_or_error.is_error()) {
|
||||
warnln("Couldn't open '{}': {}", m_name, file_or_error.error());
|
||||
// This is okay though, we'll just go with an empty document and create the file when saving.
|
||||
} else {
|
||||
auto& file = *file_or_error.value();
|
||||
m_document->set_text(file.read_all());
|
||||
}
|
||||
}
|
||||
return *m_document;
|
||||
}
|
||||
|
||||
int ProjectFile::vertical_scroll_value() const
|
||||
{
|
||||
return m_vertical_scroll_value;
|
||||
}
|
||||
|
||||
void ProjectFile::vertical_scroll_value(int vertical_scroll_value)
|
||||
{
|
||||
m_vertical_scroll_value = vertical_scroll_value;
|
||||
}
|
||||
|
||||
int ProjectFile::horizontal_scroll_value() const
|
||||
{
|
||||
return m_horizontal_scroll_value;
|
||||
}
|
||||
|
||||
void ProjectFile::horizontal_scroll_value(int horizontal_scroll_value)
|
||||
{
|
||||
m_horizontal_scroll_value = horizontal_scroll_value;
|
||||
}
|
||||
|
||||
}
|
62
Userland/DevTools/HackStudio/ProjectFile.h
Normal file
62
Userland/DevTools/HackStudio/ProjectFile.h
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CodeDocument.h"
|
||||
#include <AK/ByteBuffer.h>
|
||||
#include <AK/NonnullRefPtr.h>
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/String.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class ProjectFile : public RefCounted<ProjectFile> {
|
||||
public:
|
||||
static NonnullRefPtr<ProjectFile> construct_with_name(const String& name)
|
||||
{
|
||||
return adopt(*new ProjectFile(name));
|
||||
}
|
||||
|
||||
const String& name() const { return m_name; }
|
||||
|
||||
GUI::TextDocument& document() const;
|
||||
|
||||
int vertical_scroll_value() const;
|
||||
void vertical_scroll_value(int);
|
||||
int horizontal_scroll_value() const;
|
||||
void horizontal_scroll_value(int);
|
||||
|
||||
private:
|
||||
explicit ProjectFile(const String& name);
|
||||
|
||||
String m_name;
|
||||
mutable RefPtr<CodeDocument> m_document;
|
||||
int m_vertical_scroll_value { 0 };
|
||||
int m_horizontal_scroll_value { 0 };
|
||||
};
|
||||
|
||||
}
|
186
Userland/DevTools/HackStudio/TerminalWrapper.cpp
Normal file
186
Userland/DevTools/HackStudio/TerminalWrapper.cpp
Normal file
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "TerminalWrapper.h"
|
||||
#include <AK/String.h>
|
||||
#include <LibCore/ConfigFile.h>
|
||||
#include <LibGUI/BoxLayout.h>
|
||||
#include <LibGUI/MessageBox.h>
|
||||
#include <LibVT/TerminalWidget.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
void TerminalWrapper::run_command(const String& command)
|
||||
{
|
||||
if (m_pid != -1) {
|
||||
GUI::MessageBox::show(window(),
|
||||
"A command is already running in this TerminalWrapper",
|
||||
"Can't run command",
|
||||
GUI::MessageBox::Type::Error);
|
||||
return;
|
||||
}
|
||||
|
||||
int ptm_fd = posix_openpt(O_RDWR | O_CLOEXEC);
|
||||
if (ptm_fd < 0) {
|
||||
perror("posix_openpt");
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
if (grantpt(ptm_fd) < 0) {
|
||||
perror("grantpt");
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
if (unlockpt(ptm_fd) < 0) {
|
||||
perror("unlockpt");
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
m_terminal_widget->set_pty_master_fd(ptm_fd);
|
||||
m_terminal_widget->on_command_exit = [this] {
|
||||
int wstatus;
|
||||
int rc = waitpid(m_pid, &wstatus, 0);
|
||||
if (rc < 0) {
|
||||
perror("waitpid");
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
if (WIFEXITED(wstatus)) {
|
||||
m_terminal_widget->inject_string(String::formatted("\033[{};1m(Command exited with code {})\033[0m\n", wstatus == 0 ? 32 : 31, WEXITSTATUS(wstatus)));
|
||||
} else if (WIFSTOPPED(wstatus)) {
|
||||
m_terminal_widget->inject_string("\033[34;1m(Command stopped!)\033[0m\n");
|
||||
} else if (WIFSIGNALED(wstatus)) {
|
||||
m_terminal_widget->inject_string(String::formatted("\033[34;1m(Command signaled with {}!)\033[0m\n", strsignal(WTERMSIG(wstatus))));
|
||||
}
|
||||
m_pid = -1;
|
||||
|
||||
if (on_command_exit)
|
||||
on_command_exit();
|
||||
};
|
||||
|
||||
m_pid = fork();
|
||||
if (m_pid == 0) {
|
||||
// Create a new process group.
|
||||
setsid();
|
||||
|
||||
const char* tty_name = ptsname(ptm_fd);
|
||||
if (!tty_name) {
|
||||
perror("ptsname");
|
||||
exit(1);
|
||||
}
|
||||
close(ptm_fd);
|
||||
int pts_fd = open(tty_name, O_RDWR);
|
||||
if (pts_fd < 0) {
|
||||
perror("open");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
tcsetpgrp(pts_fd, getpid());
|
||||
|
||||
// NOTE: It's okay if this fails.
|
||||
int rc = ioctl(0, TIOCNOTTY);
|
||||
|
||||
close(0);
|
||||
close(1);
|
||||
close(2);
|
||||
|
||||
rc = dup2(pts_fd, 0);
|
||||
if (rc < 0) {
|
||||
perror("dup2");
|
||||
exit(1);
|
||||
}
|
||||
rc = dup2(pts_fd, 1);
|
||||
if (rc < 0) {
|
||||
perror("dup2");
|
||||
exit(1);
|
||||
}
|
||||
rc = dup2(pts_fd, 2);
|
||||
if (rc < 0) {
|
||||
perror("dup2");
|
||||
exit(1);
|
||||
}
|
||||
rc = close(pts_fd);
|
||||
if (rc < 0) {
|
||||
perror("close");
|
||||
exit(1);
|
||||
}
|
||||
rc = ioctl(0, TIOCSCTTY);
|
||||
if (rc < 0) {
|
||||
perror("ioctl(TIOCSCTTY)");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
setenv("TERM", "xterm", true);
|
||||
|
||||
auto parts = command.split(' ');
|
||||
ASSERT(!parts.is_empty());
|
||||
const char** args = (const char**)calloc(parts.size() + 1, sizeof(const char*));
|
||||
for (size_t i = 0; i < parts.size(); i++) {
|
||||
args[i] = parts[i].characters();
|
||||
}
|
||||
rc = execvp(args[0], const_cast<char**>(args));
|
||||
if (rc < 0) {
|
||||
perror("execve");
|
||||
exit(1);
|
||||
}
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
// (In parent process)
|
||||
terminal().scroll_to_bottom();
|
||||
}
|
||||
|
||||
void TerminalWrapper::kill_running_command()
|
||||
{
|
||||
ASSERT(m_pid != -1);
|
||||
|
||||
// Kill our child process and its whole process group.
|
||||
[[maybe_unused]] auto rc = killpg(m_pid, SIGTERM);
|
||||
}
|
||||
|
||||
TerminalWrapper::TerminalWrapper(bool user_spawned)
|
||||
: m_user_spawned(user_spawned)
|
||||
{
|
||||
set_layout<GUI::VerticalBoxLayout>();
|
||||
|
||||
RefPtr<Core::ConfigFile> config = Core::ConfigFile::get_for_app("Terminal");
|
||||
m_terminal_widget = add<TerminalWidget>(-1, false, config);
|
||||
|
||||
if (user_spawned)
|
||||
run_command("Shell");
|
||||
}
|
||||
|
||||
TerminalWrapper::~TerminalWrapper()
|
||||
{
|
||||
}
|
||||
|
||||
}
|
56
Userland/DevTools/HackStudio/TerminalWrapper.h
Normal file
56
Userland/DevTools/HackStudio/TerminalWrapper.h
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibGUI/Widget.h>
|
||||
|
||||
class TerminalWidget;
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class TerminalWrapper final : public GUI::Widget {
|
||||
C_OBJECT(TerminalWrapper)
|
||||
public:
|
||||
virtual ~TerminalWrapper() override;
|
||||
|
||||
void run_command(const String&);
|
||||
void kill_running_command();
|
||||
|
||||
bool user_spawned() const { return m_user_spawned; }
|
||||
TerminalWidget& terminal() { return *m_terminal_widget; }
|
||||
|
||||
Function<void()> on_command_exit;
|
||||
|
||||
private:
|
||||
explicit TerminalWrapper(bool user_spawned = true);
|
||||
|
||||
RefPtr<TerminalWidget> m_terminal_widget;
|
||||
pid_t m_pid { -1 };
|
||||
bool m_user_spawned { true };
|
||||
};
|
||||
|
||||
}
|
63
Userland/DevTools/HackStudio/Tool.h
Normal file
63
Userland/DevTools/HackStudio/Tool.h
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Noncopyable.h>
|
||||
#include <LibGUI/Forward.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class FormEditorWidget;
|
||||
|
||||
class Tool {
|
||||
AK_MAKE_NONCOPYABLE(Tool);
|
||||
AK_MAKE_NONMOVABLE(Tool);
|
||||
|
||||
public:
|
||||
virtual ~Tool() { }
|
||||
|
||||
virtual void on_mousedown(GUI::MouseEvent&) = 0;
|
||||
virtual void on_mouseup(GUI::MouseEvent&) = 0;
|
||||
virtual void on_mousemove(GUI::MouseEvent&) = 0;
|
||||
virtual void on_keydown(GUI::KeyEvent&) = 0;
|
||||
virtual void on_second_paint(GUI::Painter&, GUI::PaintEvent&) { }
|
||||
|
||||
virtual const char* class_name() const = 0;
|
||||
|
||||
virtual void attach() { }
|
||||
virtual void detach() { }
|
||||
|
||||
protected:
|
||||
explicit Tool(FormEditorWidget& editor)
|
||||
: m_editor(editor)
|
||||
{
|
||||
}
|
||||
|
||||
FormEditorWidget& m_editor;
|
||||
};
|
||||
|
||||
}
|
52
Userland/DevTools/HackStudio/WidgetTool.cpp
Normal file
52
Userland/DevTools/HackStudio/WidgetTool.cpp
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "WidgetTool.h"
|
||||
#include <AK/LogStream.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
void WidgetTool::on_mousedown([[maybe_unused]] GUI::MouseEvent& event)
|
||||
{
|
||||
dbgln("WidgetTool::on_mousedown");
|
||||
}
|
||||
|
||||
void WidgetTool::on_mouseup([[maybe_unused]] GUI::MouseEvent& event)
|
||||
{
|
||||
dbgln("WidgetTool::on_mouseup");
|
||||
}
|
||||
|
||||
void WidgetTool::on_mousemove([[maybe_unused]] GUI::MouseEvent& event)
|
||||
{
|
||||
dbgln("WidgetTool::on_mousemove");
|
||||
}
|
||||
|
||||
void WidgetTool::on_keydown([[maybe_unused]] GUI::KeyEvent& event)
|
||||
{
|
||||
dbgln("WidgetTool::on_keydown");
|
||||
}
|
||||
|
||||
}
|
52
Userland/DevTools/HackStudio/WidgetTool.h
Normal file
52
Userland/DevTools/HackStudio/WidgetTool.h
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Tool.h"
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class WidgetTool final : public Tool {
|
||||
public:
|
||||
explicit WidgetTool(FormEditorWidget& editor, const GUI::WidgetClassRegistration& meta_class)
|
||||
: Tool(editor)
|
||||
, m_meta_class(meta_class)
|
||||
{
|
||||
}
|
||||
virtual ~WidgetTool() override { }
|
||||
|
||||
private:
|
||||
virtual const char* class_name() const override { return "WidgetTool"; }
|
||||
virtual void on_mousedown(GUI::MouseEvent&) override;
|
||||
virtual void on_mouseup(GUI::MouseEvent&) override;
|
||||
virtual void on_mousemove(GUI::MouseEvent&) override;
|
||||
virtual void on_keydown(GUI::KeyEvent&) override;
|
||||
|
||||
const GUI::WidgetClassRegistration& m_meta_class;
|
||||
};
|
||||
|
||||
}
|
117
Userland/DevTools/HackStudio/WidgetTreeModel.cpp
Normal file
117
Userland/DevTools/HackStudio/WidgetTreeModel.cpp
Normal file
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "WidgetTreeModel.h"
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibGUI/Widget.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
WidgetTreeModel::WidgetTreeModel(GUI::Widget& root)
|
||||
: m_root(root)
|
||||
{
|
||||
m_widget_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/inspector-object.png"));
|
||||
}
|
||||
|
||||
WidgetTreeModel::~WidgetTreeModel()
|
||||
{
|
||||
}
|
||||
|
||||
GUI::ModelIndex WidgetTreeModel::index(int row, int column, const GUI::ModelIndex& parent) const
|
||||
{
|
||||
if (!parent.is_valid()) {
|
||||
return create_index(row, column, m_root.ptr());
|
||||
}
|
||||
auto& parent_node = *static_cast<GUI::Widget*>(parent.internal_data());
|
||||
return create_index(row, column, parent_node.child_widgets().at(row));
|
||||
}
|
||||
|
||||
GUI::ModelIndex WidgetTreeModel::parent_index(const GUI::ModelIndex& index) const
|
||||
{
|
||||
if (!index.is_valid())
|
||||
return {};
|
||||
auto& widget = *static_cast<GUI::Widget*>(index.internal_data());
|
||||
if (&widget == m_root.ptr())
|
||||
return {};
|
||||
|
||||
if (widget.parent_widget() == m_root.ptr())
|
||||
return create_index(0, 0, m_root.ptr());
|
||||
|
||||
// Walk the grandparent's children to find the index of widget's parent in its parent.
|
||||
// (This is needed to produce the row number of the GUI::ModelIndex corresponding to widget's parent.)
|
||||
int grandparent_child_index = 0;
|
||||
for (auto& grandparent_child : widget.parent_widget()->parent_widget()->child_widgets()) {
|
||||
if (grandparent_child == widget.parent_widget())
|
||||
return create_index(grandparent_child_index, 0, widget.parent_widget());
|
||||
++grandparent_child_index;
|
||||
}
|
||||
|
||||
ASSERT_NOT_REACHED();
|
||||
return {};
|
||||
}
|
||||
|
||||
int WidgetTreeModel::row_count(const GUI::ModelIndex& index) const
|
||||
{
|
||||
if (!index.is_valid())
|
||||
return 1;
|
||||
auto& widget = *static_cast<GUI::Widget*>(index.internal_data());
|
||||
return widget.child_widgets().size();
|
||||
}
|
||||
|
||||
int WidgetTreeModel::column_count(const GUI::ModelIndex&) const
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
GUI::Variant WidgetTreeModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
|
||||
{
|
||||
auto* widget = static_cast<GUI::Widget*>(index.internal_data());
|
||||
if (role == GUI::ModelRole::Icon) {
|
||||
return m_widget_icon;
|
||||
}
|
||||
if (role == GUI::ModelRole::Display) {
|
||||
return String::formatted("{} ({})", widget->class_name(), widget->relative_rect());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void WidgetTreeModel::update()
|
||||
{
|
||||
did_update();
|
||||
}
|
||||
|
||||
GUI::ModelIndex WidgetTreeModel::index_for_widget(GUI::Widget& widget) const
|
||||
{
|
||||
int parent_child_index = 0;
|
||||
for (auto& parent_child : widget.parent_widget()->child_widgets()) {
|
||||
if (parent_child == &widget)
|
||||
return create_index(parent_child_index, 0, &widget);
|
||||
++parent_child_index;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
55
Userland/DevTools/HackStudio/WidgetTreeModel.h
Normal file
55
Userland/DevTools/HackStudio/WidgetTreeModel.h
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibGUI/Model.h>
|
||||
#include <LibGUI/Painter.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class WidgetTreeModel final : public GUI::Model {
|
||||
public:
|
||||
static NonnullRefPtr<WidgetTreeModel> create(GUI::Widget& root) { return adopt(*new WidgetTreeModel(root)); }
|
||||
virtual ~WidgetTreeModel() override;
|
||||
|
||||
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
|
||||
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
|
||||
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
|
||||
virtual GUI::ModelIndex index(int row, int column, const GUI::ModelIndex& parent = GUI::ModelIndex()) const override;
|
||||
virtual GUI::ModelIndex parent_index(const GUI::ModelIndex&) const override;
|
||||
virtual void update() override;
|
||||
|
||||
GUI::ModelIndex index_for_widget(GUI::Widget&) const;
|
||||
|
||||
private:
|
||||
explicit WidgetTreeModel(GUI::Widget&);
|
||||
|
||||
NonnullRefPtr<GUI::Widget> m_root;
|
||||
GUI::Icon m_widget_icon;
|
||||
};
|
||||
|
||||
}
|
171
Userland/DevTools/HackStudio/main.cpp
Normal file
171
Userland/DevTools/HackStudio/main.cpp
Normal file
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "HackStudio.h"
|
||||
#include "HackStudioWidget.h"
|
||||
#include "Project.h"
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibCore/ArgsParser.h>
|
||||
#include <LibCore/Event.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibGUI/Application.h>
|
||||
#include <LibGUI/MenuBar.h>
|
||||
#include <LibGUI/MessageBox.h>
|
||||
#include <LibGUI/Widget.h>
|
||||
#include <LibGUI/Window.h>
|
||||
#include <LibThread/Lock.h>
|
||||
#include <LibThread/Thread.h>
|
||||
#include <LibVT/TerminalWidget.h>
|
||||
#include <fcntl.h>
|
||||
#include <spawn.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
using namespace HackStudio;
|
||||
|
||||
static RefPtr<GUI::Window> s_window;
|
||||
static RefPtr<HackStudioWidget> s_hack_studio_widget;
|
||||
|
||||
static bool make_is_available();
|
||||
static void update_path_environment_variable();
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (pledge("stdio tty accept rpath cpath wpath shared_buffer proc exec unix fattr thread unix sendfd ptrace", nullptr) < 0) {
|
||||
perror("pledge");
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto app = GUI::Application::construct(argc, argv);
|
||||
|
||||
if (pledge("stdio tty accept rpath cpath wpath shared_buffer proc exec fattr thread unix sendfd ptrace", nullptr) < 0) {
|
||||
perror("pledge");
|
||||
return 1;
|
||||
}
|
||||
|
||||
s_window = GUI::Window::construct();
|
||||
s_window->resize(840, 600);
|
||||
s_window->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-hack-studio.png"));
|
||||
|
||||
update_path_environment_variable();
|
||||
|
||||
if (!make_is_available())
|
||||
GUI::MessageBox::show(s_window, "The 'make' command is not available. You probably want to install the binutils, gcc, and make ports from the root of the Serenity repository.", "Error", GUI::MessageBox::Type::Error);
|
||||
|
||||
const char* path_argument = nullptr;
|
||||
Core::ArgsParser args_parser;
|
||||
args_parser.add_positional_argument(path_argument, "Path to a workspace or a file", "path", Core::ArgsParser::Required::No);
|
||||
args_parser.parse(argc, argv);
|
||||
|
||||
auto argument_absolute_path = Core::File::real_path_for(path_argument);
|
||||
|
||||
auto project_path = argument_absolute_path;
|
||||
if (argument_absolute_path.is_null())
|
||||
project_path = Core::File::real_path_for(".");
|
||||
|
||||
s_hack_studio_widget = s_window->set_main_widget<HackStudioWidget>(project_path);
|
||||
|
||||
s_window->set_title(String::formatted("{} - Hack Studio", s_hack_studio_widget->project().name()));
|
||||
|
||||
auto menubar = GUI::MenuBar::construct();
|
||||
s_hack_studio_widget->initialize_menubar(menubar);
|
||||
app->set_menubar(menubar);
|
||||
|
||||
s_window->show();
|
||||
|
||||
s_hack_studio_widget->update_actions();
|
||||
|
||||
return app->exec();
|
||||
}
|
||||
|
||||
static bool make_is_available()
|
||||
{
|
||||
pid_t pid;
|
||||
const char* argv[] = { "make", "--version", nullptr };
|
||||
posix_spawn_file_actions_t action;
|
||||
posix_spawn_file_actions_init(&action);
|
||||
posix_spawn_file_actions_addopen(&action, STDOUT_FILENO, "/dev/null", O_WRONLY, 0);
|
||||
|
||||
if ((errno = posix_spawnp(&pid, "make", &action, nullptr, const_cast<char**>(argv), environ))) {
|
||||
perror("posix_spawn");
|
||||
return false;
|
||||
}
|
||||
int wstatus;
|
||||
waitpid(pid, &wstatus, 0);
|
||||
posix_spawn_file_actions_destroy(&action);
|
||||
return WEXITSTATUS(wstatus) == 0;
|
||||
}
|
||||
|
||||
static void update_path_environment_variable()
|
||||
{
|
||||
StringBuilder path;
|
||||
path.append(getenv("PATH"));
|
||||
if (path.length())
|
||||
path.append(":");
|
||||
path.append("/bin:/usr/bin:/usr/local/bin");
|
||||
setenv("PATH", path.to_string().characters(), true);
|
||||
}
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
GUI::TextEditor& current_editor()
|
||||
{
|
||||
return s_hack_studio_widget->current_editor();
|
||||
}
|
||||
|
||||
void open_file(const String& file_name)
|
||||
{
|
||||
return s_hack_studio_widget->open_file(file_name);
|
||||
}
|
||||
|
||||
RefPtr<EditorWrapper> current_editor_wrapper()
|
||||
{
|
||||
if (!s_hack_studio_widget)
|
||||
return nullptr;
|
||||
return s_hack_studio_widget->current_editor_wrapper();
|
||||
}
|
||||
|
||||
Project& project()
|
||||
{
|
||||
return s_hack_studio_widget->project();
|
||||
}
|
||||
|
||||
String currently_open_file()
|
||||
{
|
||||
if (!s_hack_studio_widget)
|
||||
return {};
|
||||
return s_hack_studio_widget->currently_open_file();
|
||||
}
|
||||
|
||||
void set_current_editor_wrapper(RefPtr<EditorWrapper> wrapper)
|
||||
{
|
||||
s_hack_studio_widget->set_current_editor_wrapper(wrapper);
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue